Files
stack/packages/project/README.md
jason.woltjeandClaude Opus 5.5 d4ea7e5a69 webui, bus, project: a Console editor for the project's spec and docs (row 148, #1642)
Row 148 round 3, Researcher's candidate (agents/researcher/work/queue-148/,
row148.patch 1b63164b, manifest 95ae4736, 27 paths, base 18aee8fa1).
Dewey approved round 3 (comment 28832) and Filbert approved it with no
findings (28835). Mos gave the decision 123 go on public diff 8360122b
(mos-0636): no vendored code, no innerHTML or other HTML sinks.

The Docs page #/docs lists each project's spec/north-star.md and the
Markdown files directly under docs/. Save writes the typed text as one
local commit authored by business.human, through a new human-only bus
verb project.file.save that carries the opened commit as base and
refuses stale-base if the file moved. The preview is a textarea plus a
renderer built with createElement and textContent only; raw HTML shows
as source text and images never load. CSP is unchanged.

Round 3 replaced the backreference HR regex, which overflowed the
regexp stack, with an isRule scan. Round 4 added a textContent fallback
when paintDoc throws, and the submit handler resets saving in a finally.
Mos's rule (ceiling 1 s at 1 MiB, growth x3 per doubling over 1 to
4 MiB): 130/130 and 138/138 cells pass, worst 513 ms at 1 MiB, worst
ratio x2.50, control x2.01; the 756-cell grid has 0 timeouts. Mutants
43/44, v21 equivalent.

Follow-ups: Vikunja P19 tasks 318 (Save stays off after a non-render
paint throw), 319 (dead NUL check in bus.js) and 320 (the 1 MiB + 64 KiB
buffer before the human proof, Gitea #1650).

Sage's landing gate ran on a4779d2a5 plus the candidate: 25 steps,
all rc 0 (15 packages, 10 script suites), 22:19:24Z to 22:24:09Z,
network sage-r148g_default removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 17:24:39 -05:00

173 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Project record repository — layout 1
A project's records live in a separate local git repository. This is distinct
from its code checkout (`business.projects[project].root`) and the working bare
repository used for code review. No remote is created or contacted.
Set an optional normalized absolute `repo` in the business project entry:
```json
{"projects":{"stack":{"root":"/work/code","repo":"/work/project-records"}}}
```
Without `repo`, records live at
`<dataRoot>/projects/<business>/<project>/`. The records path must not overlap any declared project's code root,
including another project's root (in either direction). Record paths and their existing ancestors must not be symlinks.
```sh
mosaic project init acme stack
mosaic project check acme stack
```
Init failure removes its own files and Git directory, allowing retry. A target
that existed empty is preserved; newly created empty parent directories are
removed. Unrelated files at the root or inside a layout directory are preserved,
so a concurrent human save can leave a non-empty target that needs inspection
before retry. Changed ownership of an initializer path refuses
`project-init-cleanup` and preserves that path for reconciliation. The private
`.git` created by this initializer is removed as a unit; external tools must not
modify Git metadata during initialization.
Both commands use the existing system and business config loaders. Init requires
an outside-agent CLI environment, a declared project and an empty directory. It
creates `main` and the first commit as the business's human, with a local
`<human>@mosaic.invalid` email. Check is read-only; it exits 2 and lists all
problems found. An otherwise valid layout with no readable HEAD commit refuses
`project-no-commit` (CLI exit 2), rather than reporting success. Usage exits 4; configuration problems exit 3. No bus is needed.
## Layout
| Path | Contents |
| --- | --- |
| `project.json` | `layoutVersion: 1`, business, project, name, createdAt |
| `spec/north-star.md` | Human-editable Markdown north star |
| `docs/` | Markdown documentation; check accepts nested docs |
| `requirements/ID.json` | One requirement per file |
| `decisions/ID.json` | One decision per file |
| `templates/` | project.json, requirement.json, decision.json, document.md, north-star.md |
Empty requirements and decisions directories contain an empty `.gitkeep`, so the
layout survives git checkout. That marker is checked separately from records.
Init makes no example requirements or fictional decisions. The initial project
name is its project ID; the user can edit the display name in project.json.
A requirement has `recordVersion: 1`, `id`, `title`, `text`, and `status`
(`draft`, `accepted`, `complete`). A decision has `recordVersion: 1`, `id`,
`question`, `options` (2–9 distinct `{key,text}` entries), `recommendation`,
`status` (`open`, `accepted`, `rejected`) and `choice` (null while open, an option
key when closed). These are local reference records, not bus authorization or
approval. Decision spec citations and settlement policy belong to row 138.
Schemas are fixed in `packages/project/schemas/`, never loaded from the project.
Every JSON template validates against its record schema. Editing templates
cannot weaken validation; adding a project-local schema is refused. The schema
validator implements a closed subset of JSON Schema 2020-12 matching these
schemas and refuses any unsupported schema keyword. Dates use canonical
millisecond UTC. Files are bounded to 1 MiB, valid UTF-8 without NULs, regular,
and not hard-linked. JSON record names must equal their IDs. Unknown top-level
paths, missing required paths, unexpected templates and non-Markdown docs refuse.
The git directory is inspected separately, without reading project data as git
configuration or running hooks.
## Shared APIs
Import from `packages/project/src/index.mjs`:
```js
const context = { business, project: 'stack', dataRoot };
projectRepo(context); // absolute repo path; no writes
await initProject(context); // {repo, commit}
await checkProject(context); // {ok, repo, problems}
validateRecord('requirement', value); // array of problem strings
await writeRecord({
...context,
path: 'requirements/REQ-PROJECT-1.json',
record: value,
author: { definition: 'coder', instance: 'builder' },
}); // {repo, path, commit}
```
`writeRecord` is the single agent record writer. It accepts `docs/NAME.md`,
`requirements/ID.json` and `decisions/ID.json`, validates first, and makes a
local commit authored and committed as `<definition>.<instance>` with a local
`.invalid` email. Actor metadata is trusted caller input whose shape is
validated here; row 138's broker must derive it from the bound role. This
package neither authenticates an agent nor grants it authority.
An author with definition `human` refuses `project-author`; this agent writer
provides no human record write path. Every agent path under `spec/` refuses with `project-spec-human-only`. Agents
cannot write project.json, templates or git metadata through this API. A valid
but unchanged record refuses `project-record-unchanged` without an empty commit.
Stricter safeguards agreed with Sage (sage-0400): a remote refuses
`project-remote`; pending tracked, staged, untracked or ignored files and hidden
index flags refuse `project-dirty`. The writer never folds human edits into its
commit. Concurrent writers refuse `project-busy` through an exclusive local
lock; no stale lock is automatically removed. Links, indirect git stores and
unsupported local git config refuse. Git uses no inherited global/system
configuration, no hooks, fsmonitor, filters or transports, and runs in bounded
process groups using the existing seat git runner.
Records publish with a temporary file, fsync and rename, then a commit of only
that path. On git failure with HEAD unchanged, only the writer's file/index
change is restored. If HEAD changed while an error occurred, the writer refuses
`project-commit-uncertain` and preserves evidence for reconciliation; it never
rewinds history. Locking serializes this writer, not arbitrary human editors:
callers must coordinate simultaneous human saves. No destructive repair, push,
remote creation, role grant, spec-confirmation mechanism or live action ships.
Tests: `node --test packages/project/tests/*.test.mjs`. Fixtures only.
## Committed file reads (row 138)
`readProjectFile({business,project,dataRoot,path,commit?})` reads only
spec/north-star.md or a named docs/NAME.md; `readSpec({business,project,dataRoot,commit?})`
fixes the spec path. Both are async and read-only, share repository/path checks,
refuse remotes and unsafe Git metadata, and validate committed project.json
identity. Working-tree edits do not replace committed content. A supplied full
SHA-1 must be a commit reachable from a captured HEAD.
The result is {repo,path,head,revision,commit,content,author,log,truncated}.
commit is the last commit changing the file at revision, not necessarily HEAD;
content preserves exact valid UTF-8 including trailing newlines, up to 1 MiB with
no NUL. Log has at most 100 entries, newest first, each
{commit,author:{name,email},at,subject}; at is canonical UTC. truncated says more
entries exist. Reads reject nonregular Git blobs, missing paths and invalid
content. History output is separately bounded to 256 KiB.
readSpec adds confirmed=true exactly when the file commit author.name equals
business.human. An agent-authored latest change stays unconfirmed. This is local
Git provenance, not a cryptographic identity proof. Broker citation validation
compares a fresh current snapshot and never uses an older read as authority.
Read errors are ProjectError: project-read-path, project-read-commit,
project-read-missing, project-read-content, project-invalid, plus existing
project-id/path/not-declared/repository/remote/git codes. These reads write
nothing; the human writer is the next section.
## The human editor writer (row 148)
`listProjectFiles({business,project,dataRoot})` returns `{head, files:[{path}]}`:
spec/north-star.md and the Markdown files directly under docs/ (name
`[A-Za-z0-9][A-Za-z0-9._-]{0,127}.md`), as committed at HEAD and in path order. An
uncommitted file, a subdirectory, a non-Markdown file and a link are not listed.
`documentPath(path)` is the one definition of those paths, shared by the reads and
the writer.
`writeHumanFile({business,project,dataRoot,path,content,base})` is the Console
editor's save. It replaces the whole text of an existing document with `content` and
makes one local commit, `Edit <path>`, authored by `business.human`. Only the human
bus verb `project.file.save` calls it; the agent writer (`writeRecord`) is
unchanged and still refuses spec/. It never creates a file, never pushes and
creates no remote.
`base` is the full commit the editor opened: the file's last commit at HEAD. If the
file's last commit is any other, the writer refuses `stale-base` and writes nothing,
whoever made the newer commit. The check runs under the same exclusive lock and the
same clean-tree rules as the agent writer, so a tree with pending changes refuses
`project-dirty` and a concurrent writer `project-busy`. Other refusals:
`project-record-path` (a path outside the two), `project-record-invalid` (empty
content, a NUL, text that is not well-formed UTF-8, over 1 MiB, or a `base` that is
not a full commit id) and `project-read-missing` (no such document).
Tests: `packages/project/tests/human-file.test.mjs`.