Row 148 round 3, Researcher's candidate (agents/researcher/work/queue-148/, row148.patch 1b63164b, manifest 95ae4736, 27 paths, base18aee8fa1). 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 ona4779d2a5plus 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]>
173 lines
9.4 KiB
Markdown
173 lines
9.4 KiB
Markdown
# 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`.
|