diff --git a/.aoci/baseline.json b/.aoci/baseline.json index cacb1a9..0c43e24 100644 --- a/.aoci/baseline.json +++ b/.aoci/baseline.json @@ -1,7 +1,7 @@ { "version": 1, "created_at": "2026-08-07T17:44:36Z", - "updated_at": "2026-09-25T10:48:53Z", + "updated_at": "2026-09-26T03:19:58Z", "files": { ".gitattributes": { "role": "index", @@ -132,9 +132,9 @@ }, "aoci.code.txt": { "role": "index", - "sha256": "e4fc3abc68f9f19097ec85223417a446e6b00478e99809496b9129c375a2efb4", - "size": 191213, - "normalized_sha256": "e4fc3abc68f9f19097ec85223417a446e6b00478e99809496b9129c375a2efb4" + "sha256": "e9a60ad7224e75b5694988114fdc24004baf3f64fe40119d2b74cf494b17f935", + "size": 191550, + "normalized_sha256": "e9a60ad7224e75b5694988114fdc24004baf3f64fe40119d2b74cf494b17f935" }, "aoci.meta.txt": { "role": "index", @@ -180,9 +180,9 @@ }, "docs/cognition-volumes.md": { "role": "index", - "sha256": "4b563cc47d81df6e793b47c4200764faa14586c449ae9fb993bc0f22b831e5aa", - "size": 21699, - "normalized_sha256": "4b563cc47d81df6e793b47c4200764faa14586c449ae9fb993bc0f22b831e5aa" + "sha256": "f565ef1001c383d86beb40f7f03a6a0dd5373c12979bde35de7532922301014f", + "size": 22141, + "normalized_sha256": "f565ef1001c383d86beb40f7f03a6a0dd5373c12979bde35de7532922301014f" }, "docs/contract-authority.md": { "role": "index", @@ -248,9 +248,10 @@ "normalized_sha256": "57fbef1fc138b16d654289b3e7e58f1b16847d934166d3fa821f56d73fbd7820" }, "docs/troubleshooting.md": { - "sha256": "37734d67e3518d7b3e5ceba5fb521bb009ccf50691f3f6b07cc6231e913ade15", - "size": 20670, - "normalized_sha256": "37734d67e3518d7b3e5ceba5fb521bb009ccf50691f3f6b07cc6231e913ade15" + "role": "index", + "sha256": "d4474b4dc079558fe9e5b5f9f4cb33fa8405cc372365f0b6a0d50ebe16968095", + "size": 21149, + "normalized_sha256": "d4474b4dc079558fe9e5b5f9f4cb33fa8405cc372365f0b6a0d50ebe16968095" }, "docs/uninstall.md": { "role": "index", @@ -259,9 +260,10 @@ "normalized_sha256": "16a03fde9dcc18e069bce9960f712b93ce756cb8d7e9e693db3dfab9475d2336" }, "docs/upgrading.md": { - "sha256": "5051a5a975c601da79982a86be55c85f825e7fd96ae16d2709406c5809a7d8ff", - "size": 5619, - "normalized_sha256": "5051a5a975c601da79982a86be55c85f825e7fd96ae16d2709406c5809a7d8ff" + "role": "index", + "sha256": "e623eb3e3201c304788aa194e89be34077242a0695e8b87d54f33d6f95a0b6bd", + "size": 6398, + "normalized_sha256": "e623eb3e3201c304788aa194e89be34077242a0695e8b87d54f33d6f95a0b6bd" }, "docs/windows-host-agent.en.md": { "role": "index", @@ -1947,6 +1949,14 @@ "format_sha256": "98bab51994cd2c4d45b5d1f348c32b1caf67b805c5f05917165c84e7641ca3af", "format_kind": "gofmt" }, + "internal/cli/init_neutral_root_test.go": { + "role": "observe", + "sha256": "b81a098e583cd62b14ba606206fa353173c595e79f2e8f836cdf2a8fd40d8564", + "size": 2165, + "normalized_sha256": "b81a098e583cd62b14ba606206fa353173c595e79f2e8f836cdf2a8fd40d8564", + "format_sha256": "b81a098e583cd62b14ba606206fa353173c595e79f2e8f836cdf2a8fd40d8564", + "format_kind": "gofmt" + }, "internal/cli/init_opencode_test.go": { "role": "observe", "sha256": "053cd3c01e63de0c7737ee4c4c46f928e619ae3296a21fb0958cc4299bdade75", @@ -1973,10 +1983,10 @@ }, "internal/cli/init_volumes.go": { "role": "index", - "sha256": "46713bbfdecea6892de7591e80938b46a00f8a252f6cd68df5dd244f3cfa2279", - "size": 4292, - "normalized_sha256": "46713bbfdecea6892de7591e80938b46a00f8a252f6cd68df5dd244f3cfa2279", - "format_sha256": "46713bbfdecea6892de7591e80938b46a00f8a252f6cd68df5dd244f3cfa2279", + "sha256": "5abf8dbb685b2a9b8d7c26ef3afe2b3ad704f89595168fa8f17a713eff986d69", + "size": 4948, + "normalized_sha256": "5abf8dbb685b2a9b8d7c26ef3afe2b3ad704f89595168fa8f17a713eff986d69", + "format_sha256": "5abf8dbb685b2a9b8d7c26ef3afe2b3ad704f89595168fa8f17a713eff986d69", "format_kind": "gofmt" }, "internal/cli/json_ascii.go": { @@ -3726,10 +3736,10 @@ }, "internal/index/editor_remove.go": { "role": "index", - "sha256": "f700dfb6b3243708494f71b1cff1ad965ea6b3a166172df7c5c637193f113cad", - "size": 7479, - "normalized_sha256": "f700dfb6b3243708494f71b1cff1ad965ea6b3a166172df7c5c637193f113cad", - "format_sha256": "f700dfb6b3243708494f71b1cff1ad965ea6b3a166172df7c5c637193f113cad", + "sha256": "34265f95d0fe25481f7ba8d22ca277be42a2ccc89989fb6917771c8ae5ba489a", + "size": 7728, + "normalized_sha256": "34265f95d0fe25481f7ba8d22ca277be42a2ccc89989fb6917771c8ae5ba489a", + "format_sha256": "34265f95d0fe25481f7ba8d22ca277be42a2ccc89989fb6917771c8ae5ba489a", "format_kind": "gofmt" }, "internal/index/editor_remove_test.go": { @@ -3812,11 +3822,28 @@ "format_sha256": "2bb9350efe2d11b974dfcec1b7b855c98bd51bc78c71ce1fc72c99f0bbb67695", "format_kind": "gofmt" }, + "internal/index/neutral_root.go": { + "role": "index", + "sha256": "c3d19db85b31b7c6704fd211d56d0b2ca0d0517545ef06564ea6dfe6a438f073", + "size": 1152, + "normalized_sha256": "c3d19db85b31b7c6704fd211d56d0b2ca0d0517545ef06564ea6dfe6a438f073", + "format_sha256": "c3d19db85b31b7c6704fd211d56d0b2ca0d0517545ef06564ea6dfe6a438f073", + "format_kind": "gofmt" + }, + "internal/index/neutral_root_test.go": { + "role": "observe", + "sha256": "c9818256ae91761ace13ff2a8a1f92bb171edfe511f95edc2faae944425f92d3", + "size": 3120, + "normalized_sha256": "c9818256ae91761ace13ff2a8a1f92bb171edfe511f95edc2faae944425f92d3", + "format_sha256": "c9818256ae91761ace13ff2a8a1f92bb171edfe511f95edc2faae944425f92d3", + "format_kind": "gofmt" + }, "internal/index/parser.go": { - "sha256": "4e8d3da2e5a908644afc22b062ecfc83bc9c4a352ffe691a1f6fdd0db49ceb87", - "size": 29553, - "normalized_sha256": "4e8d3da2e5a908644afc22b062ecfc83bc9c4a352ffe691a1f6fdd0db49ceb87", - "format_sha256": "4e8d3da2e5a908644afc22b062ecfc83bc9c4a352ffe691a1f6fdd0db49ceb87", + "role": "index", + "sha256": "1f46e40ddd3b583ed329488240f5c34c26ae8ef0f3ed648db6870b244d75466d", + "size": 29638, + "normalized_sha256": "1f46e40ddd3b583ed329488240f5c34c26ae8ef0f3ed648db6870b244d75466d", + "format_sha256": "1f46e40ddd3b583ed329488240f5c34c26ae8ef0f3ed648db6870b244d75466d", "format_kind": "gofmt" }, "internal/index/parser_test.go": { @@ -5978,9 +6005,10 @@ "normalized_sha256": "3146ef8ce75f5bf5170091fc4f672011e51895e22157302fbb48021e380362a4" }, "scripts/blackbox/mcp_scenarios.py": { - "sha256": "ed95c7399485e2429b4fa975ccdfe91d2c3b1df62ee2a932ce1d4e906748b997", - "size": 101275, - "normalized_sha256": "ed95c7399485e2429b4fa975ccdfe91d2c3b1df62ee2a932ce1d4e906748b997" + "role": "index", + "sha256": "c39df3ce245f383c9eb909f7c0dae4a153ffaa9bff4ea16e39f765f9e1a05dd4", + "size": 100640, + "normalized_sha256": "c39df3ce245f383c9eb909f7c0dae4a153ffaa9bff4ea16e39f765f9e1a05dd4" }, "scripts/blackbox/mcp_upgrade.py": { "sha256": "9950bd30e7ca8e67a754d7c6b3804edbfc86361be421296546a7ab81866d9f63", @@ -6058,9 +6086,9 @@ }, "scripts/release/clean-room-smoke.sh": { "role": "index", - "sha256": "358e2feafec15a592c416c348eb0aeaed766a22beee19b3cb50228fd09dd5fd3", - "size": 6454, - "normalized_sha256": "358e2feafec15a592c416c348eb0aeaed766a22beee19b3cb50228fd09dd5fd3" + "sha256": "9eff8cd2fbfcc2aaac64963e906224c761a1ac428dfcd5e6540a8a146306d4fe", + "size": 6533, + "normalized_sha256": "9eff8cd2fbfcc2aaac64963e906224c761a1ac428dfcd5e6540a8a146306d4fe" }, "scripts/release/manifest/main.go": { "role": "index", @@ -6169,9 +6197,9 @@ }, "spec/public/aoci-index-format-v1.txt": { "role": "index", - "sha256": "dfc68681e863e18a59f01113fd556d5f9fef6933614eaf53a8591e10431d0598", - "size": 9421, - "normalized_sha256": "dfc68681e863e18a59f01113fd556d5f9fef6933614eaf53a8591e10431d0598" + "sha256": "bf976f0201cbe743fcdebf39ee8dd03f62387d64381cb7e46e6817cbe4ea514e", + "size": 10708, + "normalized_sha256": "bf976f0201cbe743fcdebf39ee8dd03f62387d64381cb7e46e6817cbe4ea514e" }, "spec/public/aoci-managed-scope-and-budget-v1.txt": { "role": "index", diff --git a/aoci.code.txt b/aoci.code.txt index 950bba3..348a8d5 100644 --- a/aoci.code.txt +++ b/aoci.code.txt @@ -33,7 +33,7 @@ main.go[CG8T]: F:Starts the aoci process, delegates to the CLI root command, and ===/home/alkor2000/aoci-code-public-staging-phase1/public-candidate/docs/=== agent-integrations.md[CG5M]: F:Documents safe Codex, Claude Code, Cursor, and OpenCode V1 integration, Codex compaction reload, host-config governance, and execution modes | R:code:internal/hooks/opencode.go,code:internal/hooks/codex.go,code:internal/cli/init_gitignore.go | A:aoci init --agent opencode | S:Refresh or reopen only when the session has not loaded the configured server; a hand-made host config must be ignored before the first scan; OpenCode is V1 only; Codex truncates results per model cognition-refresh.md[CG5M]: F:Defines checkpoint reasons, delivery receipts, refresh generations, the session cognition line, the probe, compaction handoff limits, and post-change alignment | R:code:spec/public/aoci-cognition-refresh-v1.txt | A:- | S:A probe is valid only when no Host compaction is known; after a known compaction the handoff carries no Index semantics and only a fresh complete Overview can restore reliability -cognition-volumes.md[CG5M]: F:Explains Volume-first init, authoring batches, lifecycle routing, next_commands, terminal proof, and database governance including constrained openGauss | R:code:spec/public/aoci-cognition-volumes-v1.txt,code:spec/public/aoci-index-format-v1.txt,code:spec/public/aoci-database-evidence-v1.txt | A:Public Volumes and lifecycle guide | S:Relations never block or replan a batch; database authoring consumes accepted saved Evidence only and never reconnects during Maintain or Apply; a batch is the team size, sized for one inline call +cognition-volumes.md[CG5M]: F:Explains Volume-first init, neutral Code roots, authoring batches, lifecycle routing, terminal proof, and database governance including constrained openGauss | R:code:spec/public/aoci-cognition-volumes-v1.txt,code:spec/public/aoci-index-format-v1.txt,code:spec/public/aoci-database-evidence-v1.txt | A:Public Volumes and lifecycle guide | S:Relations never block or replan a batch; database authoring consumes accepted saved Evidence only and never reconnects during Maintain or Apply; a batch is the team size, sized for one inline call database-cognition-authoring.md[CG5M]: F:Documents evidence-bound model authoring and atomic application of Database Cognition entries for Volumes repositories | R:code:docs/cognition-volumes.md | A:aoci_maintain,aoci_update_entry | S:Database Cognition uses ordinary no-argument Maintain and the complete current machine batch; the 64 KiB byte gate is the operative page bound; status --deep and index score remain Legacy-only database-evidence.md[CG5M]: F:Documents configuration, catalog-only collection, snapshots, Baselines, drift, and real-engine acceptance for PostgreSQL, MySQL, and openGauss | R:code:spec/public/aoci-database-evidence-v1.txt,code:internal/dbevidence/collector.go,code:internal/dbevidence/collector_opengauss.go,code:third_party/openGauss-connector-go-pq.PROVENANCE.md | A:Database Evidence developer guide | S:openGauss is exact 6.0.5 A/PG with strict remote verify-full TLS and fail-closed unsupported objects; failures never advance Evidence or Baseline getting-started.md[CG5T]: F:Guides release installation, Fresh Code-only Volumes initialization, agent integration, model-authored first-index generation, and aligned verification | R:code:docs/install.md,code:docs/cognition-volumes.md | A:- | S:Fresh Volumes: scan, then live Guide, no-argument Maintain, complete batches, then Verify, Check, and Guide; without a Baseline the Guide is blocked with a scan stop; Legacy planning is not the path @@ -44,9 +44,9 @@ overview-delivery.md[CG5S]: F:Documents Overview chunk transport, cursors, the p rollback.md[CG5T]: F:Provides safe binary rollback, conditional MCP process refresh, and layout-appropriate post-rollback verification | R:code:docs/upgrading.md | A:- | S:A host-wide restart is unnecessary when its MCP integration can reload and prove the replacement process; Volumes close through Verify, Check, and Guide, while status --deep remains Legacy-only safe-inventory-and-scope-refresh.md[CG5T]: F:Explains Safe Inventory exclusions, scope proposals, review, atomic refresh boundaries, and the approval artifact the Apply step reads | R:- | A:- | S:- supply-chain.md[CG8M]: F:Defines release assets, signing, provenance, dependency auditing with a go.mod-checked driver table, and reproducible verification of the patched Connector | R:code:.github/workflows/release.yml,code:THIRD-PARTY-NOTICES,code:scripts/check-licenses.sh,code:scripts/check-opengauss-connector.sh,code:go.mod,code:internal/dbevidence/driver_pin_contract_test.go,code:third_party/openGauss-connector-go-pq.PROVENANCE.md | A:make release-check | S:go mod verify does not cover a local replace; the connector gate must verify origin and sums, replay the canonical patch, and byte-compare the complete tree; the driver audit table is a released declaration, so a row may change only when go.mod changes with it, never by hand -troubleshooting.md[CG5M]: F:Diagnoses MCP identity, host scope, slow authoring, spills, stdio, repair, receipts, held files, drift, scope changes, worktree roots, git visibility, status | R:code:docs/upgrading.md,code:docs/cognition-volumes.md,code:internal/ui/server.go,code:internal/index/parser.go,code:internal/cognitiontxn/transaction.go | A:- | S:Disk bytes do not prove active MCP identity; the server self-reports service_binary_replaced_on_disk, and the page reads that fact from outside the process +troubleshooting.md[CG5M]: F:Diagnoses MCP identity, host scope, authoring, transport, repair, drift, scope changes, neutral and historical roots, Git visibility, and status | R:code:docs/upgrading.md,code:docs/cognition-volumes.md,code:internal/ui/server.go,code:internal/index/parser.go,code:internal/cognitiontxn/transaction.go | A:- | S:Disk bytes do not prove active MCP identity; the server self-reports service_binary_replaced_on_disk, and the page reads that fact from outside the process uninstall.md[CG5T]: F:Explains removal of the binary, host integration, and optional repository-local AOCI assets | R:- | A:- | S:- -upgrading.md[CG5T]: F:Defines safe binary upgrade, artifact verification, MCP refresh, runtime identity, Managed Scope path migration, in-flight approval limits, and rollback | R:code:docs/install.md,code:docs/rollback.md,code:spec/public/aoci-managed-scope-and-budget-v1.txt | A:- | S:An approval binds the preview envelope, not the plan, so finish or discard a pending pair before replacing the binary; rc13 and older read an rc14-created index but not its special names +upgrading.md[CG5S]: F:Defines safe binary upgrade, artifact verification, MCP refresh, scope migration, neutral-root and filename compatibility, approval limits, and rollback | R:code:docs/install.md,code:docs/rollback.md,code:spec/public/aoci-managed-scope-and-budget-v1.txt | A:- | S:Approvals bind the preview envelope, not the plan; older readers lack neutral-root collision and last-delete guarantees, and rc13 cannot read special names introduced in rc14 windows-host-agent.md[CG5L]: F:Documents Windows host integration, runtime identity, Guide execution, PowerShell-safe workflows, and the no-staging-files rule for Volumes authoring batches | R:code:docs/cognition-volumes.md | A:- | S:The Stage pipeline sections are Legacy-only; Volume-first repositories use Guide plus no-argument Maintain, and the F-length-never-blocks rule does not apply to FRAS v2 objects zh-cn-contract-authority.md[CG5T]: F:Defines the authority order for Chinese contracts, machine facts, Guide output, and runtime assets | R:- | A:- | S:- contract-authority.md[CG5T]: F:Provides the English rendering of the contract-authority order and conflict handling for fact sources | R:code:docs/zh-cn-contract-authority.md | A:- | S:- @@ -158,7 +158,7 @@ index_update_automation_review.go[CG7M]: F:Persists the Entries Auto Diff review index_update_render.go[CG7M]: F:Renders localized update plans, findings, diffs, and completion messages | R:- | A:- | S:- init_gitignore.go[CG7M]: F:Creates the default-deny .aoci/.gitignore boundary, ignores the host configuration init wrote and its backups, and normalizes line endings for new repositories | R:code:internal/fs/atomic.go,code:internal/fs/git_command.go,code:internal/cli/init.go | A:- | S:Maintainer files are never rewritten, which outranks the line-ending protection; the host block must precede the first scan, since roles freeze there and later removal needs an approved Scope Change init_messages.go[CG7S]: F:Renders deterministic localized initialization results and next steps | R:- | A:- | S:- -init_volumes.go[CG7S]: F:Creates parseable Root, Meta, and Code Volume skeletons for new repositories | R:- | A:- | S:Meta and empty Code are created before Root activation; retries accept only exact deterministic regular-file postimages, and init never authors Code Entries or Database semantics +init_volumes.go[CG7S]: F:Creates Root, Meta, and neutral-root Code skeletons and completes exact interrupted initialization postimages | R:code:internal/index/neutral_root.go | A:- | S:Meta and Code precede Root activation; an older marker-only Code skeleton must retain its bytes and Baseline hash on retry; init authors no Entry semantics json_ascii.go[CG7S]: F:Encodes stable machine JSON with ASCII-safe escaping where protocol compatibility requires it | R:- | A:- | S:- json_error.go[CG7S]: F:Maps CLI failures to stable machine-readable error envelopes and exit codes | R:- | A:- | S:MachineCode overrides generic exit mapping; successful --json payloads remain unwrapped, and this envelope is emitted only before any business JSON was written locale_migration.go[CG7L]: F:Prepares resumable Locale-migration receipts and validates that staged Header and Entry changes preserve formal topology and source-backed technical facts | R:code:internal/config/config.go,code:internal/index/parser.go,code:internal/hooks/installer.go | A:- | S:Preparation changes configuration only; ordinary Entries remain byte-identical until source-bound Entry stages, while legacy .aoci governance Entries are removed during Header reclassification @@ -248,18 +248,19 @@ git_ignore.go[CG7T]: F:Answers whether a path is hidden by Git's ignore authorit ===/home/alkor2000/aoci-code-public-staging-phase1/public-candidate/internal/index/=== dict.go[CG7L]: F:Parses Header and Meta tag dictionaries, validates canonical compact tag components, and exposes exact symbol definitions for semantic calibration | R:code:internal/authoringcontract/contract.go | A:ExtractTagDict,ExtractScopedTagDict,CheckTagsAgainstDict,DimensionNameNearMiss,AcceptedDimensionSpellingsForLocale,DisplayDimensionSpelling | S:Accepted spellings are exact and a near miss is only reported; a diagnostic must carry only text its Locale accepts, including the operator's own spelling, or the runtime discards it whole editor.go[CG7L]: F:Edits or inserts one Entry while preserving section structure and unrelated index bytes | R:- | A:- | S:Transforms stay in memory; planNewSection is the one decision for writer and probe: a new index starts with its root section, sections continue the resolved root, one resolving elsewhere is refused -editor_remove.go[CG7S]: F:Removes one exact Entry preimage and prunes only directory Sections left semantically empty | R:code:internal/index/editor.go,code:internal/index/parser.go | A:RemoveEntry,RemoveEntryForPath,PruneEmptySections | S:Zero or duplicate matches fail; path-aware removal disambiguates identical lines; line endings and independent formal content are preserved; an empty root section stays while it anchors other sections +editor_remove.go[CG7M]: F:Removes one exact Entry preimage and prunes empty directory sections while retaining coordinate anchors | R:code:internal/index/editor.go,code:internal/index/parser.go,code:internal/index/neutral_root.go | A:RemoveEntry,RemoveEntryForPath,PruneEmptySections | S:A historical empty root anchors remaining sections; a neutral root must survive even the last deletion, or reinsertion records the runtime path; identical lines in different sections need path binding escale.go[CG7M]: F:Derives and validates the scale tag from governed source line-count evidence and slices one profile's dictionary text out of a Meta | R:- | A:- | S:E-scale drift is Warning-only and dictionary thresholds rule; missing, unparsable, overlapping, or gapped ranges are never guessed; CheckEScaleDetail is the one decision, CheckEScale renders it escale_path.go[CG7T]: F:Resolves source paths safely for scale validation without escaping the repository | R:- | A:- | S:Only repository-root .aoci/** and root aoci.txt are excluded to prevent governance-history and self-referential scale churn; similarly named nested or prefix paths remain checked header.go[CG7M]: F:Provides pure Header extraction, validation, replacement, and diff primitives | R:code:internal/index/parser.go,code:internal/index/editor.go | A:ExtractHeader,ValidateHeaderText,ReplaceHeader,RenderHeaderDiff | S:The first === line is the Header boundary; new nonblank lines must start with # and cannot contain section syntax, while replacement preserves Entry bytes and line endings and strips a leading BOM locale.go[CG7T]: F:Detects and constructs the canonical explicit Locale marker for an AOCI Index | R:code:textassets/catalog.go | A:DetectLocale,LocaleMarker | S:An Index without a marker deterministically uses the legacy zh-CN locale; empty, duplicate, or unsupported explicit markers fail -parser.go[CG8L]: F:Parses Legacy and Volume Entry sections and F/R/A/S fields into deterministic records | R:- | A:- | S:Section roots are historical coordinates judged from text before the runtime root, so origin and clones agree: the old writer's shape (first section full, rest under the truncated root) reads as written; 2+ sections rooted below the runtime root (nested worktree) relocate whole, a lone one stays direct; else direct matches, then only the unmatched subset relocates by a shared prefix, never a guessed path +parser.go[CG8L]: F:Parses Legacy and Volume Entry sections and F/R/A/S fields into deterministic records | R:code:internal/index/neutral_root.go | A:- | S:Exact neutral markers resolve before runtime matching; unmarked historical roots retain the old writer's text-defined full/truncated reading; 2+ sections rooted below the runtime root relocate whole, a lone one stays direct; else direct matches, then only the unmatched subset relocates by a shared prefix, never a guessed path query.go[CG7M]: F:Parses and evaluates keyword/tag Entry searches and renders localized session runtime rules with machine cognition limits | R:code:internal/index/parser.go,code:textassets/catalog.go,code:internal/machinecontract/numeric.go | A:ParseTagFilter,Search,BuildRuntimeRules | S:Only C accepts >= filters; runtime rules come from the active Locale asset and append validated refresh/chunk limits, never model-generated repository semantics quota.go[CG7M]: F:Extracts effective per-C S-field rune quotas from Meta and reports violations with machine-default gap filling | R:code:internal/index/dict.go,code:internal/machinecontract/numeric.go | A:ExtractSQuotaThresholds,EffectiveSQuotaContract,CheckSQuotaWith,LimitForC | S:CheckSQuotaWith is warning-level and honours a declaration both ways; LimitForC serves error-level gates and honours only a looser one, so narrowing a header cannot make a persisted Volume unloadable relations.go[CG5S]: F:Reports single-line form problems in a candidate R field without touching the filesystem | R:code:internal/index/validator.go | A:ValidateEntryRelations | S:Every result is a Warning that never blocks, and targets are never resolved, so R may name anything the model means types.go[CG7S]: F:Defines Header, Entry, tag, field, section, and validation data structures | R:- | A:- | S:FullLine, not parsed fields, is canonical for comparison and replacement; malformed tags degrade to an empty map; LegacyAbsPath keeps the original header reading so truncated roots still resolve validator.go[CG8M]: F:Validates Entry syntax, required fields, tags, field limits, and semantic structure | R:- | A:- | S:Multiline, filename mismatch, missing FRAS, or duplicate F/R/A are hard errors; unparseable tags, evolution narrative, and S quota are warnings, and validation never silently repairs candidate bytes validator_tagparse.go[CG7T]: F:Classifies existing validator warnings into the tagparse reporting dimension without implementing a second tag parser | R:code:internal/index/validator.go,code:internal/index/parser.go | A:HasTagParseWarning | S:Only Warning-level violations with the stable tag-parse prefix qualify; parsing authority remains ValidateEntryLineWith and ParseTags +neutral_root.go[CG7T]: F:Defines the neutral Code root marker and resolves its section family independently of the host repository path | R:code:internal/index/parser.go,code:internal/index/editor_remove.go,code:internal/cli/init_volumes.go | A:NeutralCodeRootHeader | S:Recognition must precede runtime matching: a checkout at /.code/src still contains src; only the exact named marker opts in, so historical coordinates keep their prior reading ===/home/alkor2000/aoci-code-public-staging-phase1/public-candidate/internal/machinecontract/=== capabilities.go[CG9T]: F:Defines canonical AOCI-CODE product, repository, module, binary, MCP, and capability identities | R:- | A:- | S:- @@ -343,7 +344,7 @@ check-opengauss-connector.sh[CG8S]: F:Verifies local connector identity or downl main.go[CG5M]: F:Rejects release archives unless they contain only the binary, bilingual READMEs and logos, changelog, and all five legal assets, then executes Linux amd64 | R:code:.goreleaser.yml | A:go run ./scripts/release/archive-smoke --dist | S:Every entry must be regular, and exactly one Linux amd64 archive must report an aoci version ===/home/alkor2000/aoci-code-public-staging-phase1/public-candidate/scripts/release/=== -clean-room-smoke.sh[CG8S]: F:Builds and exercises aoci in an isolated repository and verifies the bounded current-machine Volume Guide contract | R:- | A:- | S:The fixture preserves the production-role split: ordinary source enters Index, counter_test.go enters Observe, tests/fixtures remains Exclude, and the MCP surface stays at nine tools; the Guide assertion pins the machine default batch of 20 over EXPECTED_AUTHORING_TARGETS, which counts business sources plus every governed file init generates, so changing either breaks here +clean-room-smoke.sh[CG8S]: F:Builds and exercises aoci in isolation, checking neutral-root initialization, production scope roles, and bounded Volume Guide authoring | R:- | A:- | S:The fixture preserves the production-role split: ordinary source enters Index, counter_test.go enters Observe, tests/fixtures remains Exclude, and the MCP surface stays at nine tools; the Guide assertion pins the machine default batch of 20 over EXPECTED_AUTHORING_TARGETS, which counts business sources plus every governed file init generates, so changing either breaks here ===/home/alkor2000/aoci-code-public-staging-phase1/public-candidate/scripts/release/manifest/=== main.go[CG8L]: F:Generates and offline-verifies release manifest v2-v4 identities, tool contracts, artifact hashes, and signed-release descriptors | R:code:scripts/release/manifest/signed_release.go,code:scripts/release/manifest/signed_bundle.go,code:Makefile,code:internal/machinecontract/capabilities.go,code:internal/machinecontract/managed_scope.go | A:go run ./scripts/release/manifest,go run ./scripts/release/manifest --verify | S:Generation rejects dirty source and commit mismatch; signed v4 is fail-closed while legacy v2 and v3 remain verify-compatible @@ -362,7 +363,7 @@ aoci-database-evidence-v1.txt[CG8M]: F:Specifies catalog-only PostgreSQL, MySQL, aoci-database-table-fras-v1.txt[CG8T]: F:Specifies model-authored table F/R/A/S and canonical object references over accepted PostgreSQL, MySQL, or openGauss Evidence | R:code:spec/public/aoci-database-evidence-v1.txt,code:spec/public/aoci-database-cognition-authoring-v1.txt | A:Database Table FRAS v1 | S:Automatic table cognition generation and formal Database Volume creation remain outside this contract aoci-host-capability-and-interaction-v1.txt[CG8S]: F:Specifies capability discovery, host interaction, TTY approval, and machine identity contracts | R:- | A:- | S:The human interaction contract requires an actual character-device TTY and exact digest phrase; --yes, piped model approval, and approval reuse are forbidden, while Fresh policy-bound auto remains a distinct non-human mechanism aoci-host-cognition-messaging-v2.txt[CG8S]: F:Specifies concise user-facing messaging for cognition delivery and verification outcomes | R:- | A:- | S:- -aoci-index-format-v1.txt[CG8S]: F:Defines the public text interoperability contract for Cognition assets, Entries, Volume layout, directory roots, and validation compatibility | R:code:spec/public/aoci-cognition-volumes-v1.txt,code:spec/public/aoci-code-cli-runtime-v1.txt,code:internal/index/parser.go,code:internal/index/editor.go,code:internal/cognition/objects.go | A:AOCI Public Cognition Format v1 | S:Code section absolute roots are historical structural coordinates, never runtime access paths or semantic identities; readers use the invocation root to derive repository-relative CanonicalRefs, preserve formal bytes on clone, distinguish #Volume path=, and fail closed on unsafe mapping; names are never escaped: a header reads to its final slash and a bracketed file name to the tag before ': F:' +aoci-index-format-v1.txt[CG8S]: F:Defines text interoperability for Cognition assets, Entries, Volumes, neutral and historical section roots, names, and validation compatibility | R:code:spec/public/aoci-cognition-volumes-v1.txt,code:spec/public/aoci-code-cli-runtime-v1.txt,code:internal/index/parser.go,code:internal/index/editor.go,code:internal/cognition/objects.go | A:AOCI Public Cognition Format v1 | S:Section coordinates are not runtime access paths or semantic identities; only new init uses the exact neutral marker, while old indexes keep their interpretation and bytes; older readers parse it but lack overlap and last-delete guarantees; Volume path= remains repository-relative; names are never escaped aoci-managed-scope-and-budget-v1.txt[CG8M]: F:Specifies governed roles, authoring debt, coverage-reduction protection, ratifiable blockers, host-independent paths, posture, transactions, and budgets | R:code:internal/scopechange/authorization.go,code:internal/managedscope/evaluate.go | A:- | S:Refusing auto is only half the contract: every blocker independent review may ratify also sets interaction_required, while safety boundaries carry no reviewer and must be resolved instead; under Volumes the candidate set carries policy only and a changed Index source is retained under source_stale_retained, not blocked aoci-mcp-runtime-v1.txt[CG8T]: F:Specifies MCP server identity, stdio JSON-RPC behavior, and the stable nine-tool surface | R:- | A:- | S:- aoci-object-fras-v2.txt[CG8T]: F:Specifies canonical object identities, expanded starter dictionaries, compact tags, and FRAS v2 field limits | R:code:spec/public/aoci-cognition-volumes-v1.txt | A:repository-cognition-object/v2,compact A+B+C+[D]+E | S:Starter G means cross-domain and Z requires understood evidence with no named fit; insufficient evidence is never Z or an S constraint, and existing formal Meta remains authoritative @@ -612,7 +613,7 @@ README.md[CG5S]: F:Documents the four black-box verification suites, their depen generate_repo_c.py[CG5M]: F:Generates the frozen 453-file layered fixture that lets the scale suite reach the real batch limit with realistic cross-batch relations | R:- | A:python3 scripts/blackbox/generate_repo_c.py | S:Regenerating changes the fixture identity the scale suite asserts; generated names must avoid the built-in sensitive and runtime patterns, and a NUL byte makes Curation silently skip the file mcp_conformance.py[CG8L]: F:Drives the built binary as a real MCP stdio client and checks the wire surface: handshake, nine-tool registry, input schemas, response shapes, malformed input | R:code:scripts/blackbox/README.md,code:README.md,code:README.zh-CN.md,code:internal/mcptools/server.go,code:scripts/blackbox/stdio_deadline.py,code:scripts/blackbox/stdio_capture.py | A:python3 scripts/blackbox/mcp_conformance.py,AOCI_REPO,AOCI_BIN,AOCI_EXPECT_VERSION | S:Read-only: it expects zero formal writes and the host repository must already be established with a multi-chunk Overview; the 48 KiB host window is measured on a real host, not assumed; the published check count is a suite property and the run fails when any document disagrees; the Overview reader follows the exact cursor chain to the declared chunk count; rules and malformed-input checks are locale-independent mcp_lifecycle.py[CG8L]: F:Runs complete init-to-realignment lifecycles over three frozen fixture projects and an optional model track driving a real AI agent | R:code:scripts/blackbox/README.md,code:scripts/blackbox/generate_repo_c.py,code:scripts/blackbox/mcp_scenarios.py,code:internal/cli/init.go | A:python3 scripts/blackbox/mcp_lifecycle.py,--suites,--model,--compare | S:repo-a and repo-b cover drift and re-alignment, repo-c multi-batch authoring at the raised limit; the database suite needs Docker and proves current items fold out of Maintain transport against real MySQL; committed fixtures must be named where advertised; governance walks the pending-policy path (#47) and the held-source probes to aligned in auto mode, reading causes from verify since Maintain samples findings -mcp_scenarios.py[CG8L]: F:Drives hostile handling of fixtures: cursors, write rejection, crashes, races, Scope Change, activation, held sources, next commands, page, optimization | R:code:scripts/blackbox/README.md,code:README.md,code:README.zh-CN.md,code:internal/mcptools/tools_next_commands.go,code:internal/mcptools/tools_read_refresh.go,code:internal/ui/server.go | A:python3 scripts/blackbox/mcp_scenarios.py,AOCI_SCENARIO_WORK,AOCI_SCENARIO_KEEP | S:A writing scenario builds its own fixture; the host repository stays read-only; the 48 KiB host window and returned-command group are measured; the published count is enforced; tty needs a real pty; paths are slash-normalized on Windows; the page digest needs an untouched fixture; a page port is read from its readiness line; Scope Change over a changed source: Volumes retains, Legacy blocks; cli() adds --json +mcp_scenarios.py[CG8L]: F:Drives hostile handling of fixtures: cursors, write rejection, crashes, races, Scope Change, activation, held sources, next commands, page, optimization | R:code:scripts/blackbox/README.md,code:README.md,code:README.zh-CN.md,code:internal/mcptools/tools_next_commands.go,code:internal/mcptools/tools_read_refresh.go,code:internal/ui/server.go | A:python3 scripts/blackbox/mcp_scenarios.py,AOCI_SCENARIO_WORK,AOCI_SCENARIO_KEEP | S:Writing scenarios own fixtures; host stays read-only. Attestation answers strip neutral coordinates, not the host path. The 48 KiB host window and published count are enforced; TTY needs a real pty; page digest needs an untouched fixture and the readiness-reported port; changed-source Scope Change retains under Volumes and blocks under Legacy mcp_upgrade.py[CG8L]: F:Proves the upgrade axis: a repository built and authored by a previously released binary stays governable by the binary under test | R:code:scripts/blackbox/README.md,code:internal/cognitionbudget/policy.go,code:internal/config/scope_budget.go,code:.github/workflows/full-confidence.yml,code:scripts/blackbox/stdio_deadline.py,code:scripts/blackbox/stdio_capture.py,code:internal/index/parser.go | A:python3 scripts/blackbox/mcp_upgrade.py,--versions,--allow-offline,AOCI_UPGRADE_CACHE | S:Every released init writes a cognition_budget block, so only nobudget, stripped before scan, reaches LegacyPolicy; spacedroot and cutsegment carry the truncated roots releases up to rc13 wrote; worktree authors in .worktrees/wt, merges, and reads from the primary checkout; growth must read aligned in a checkout. A fetch failure fails the run: a skipped matrix is a false green; only the newest CHANGELOG entry may 404 stdio_deadline.py[CG5T]: F:Wraps MCP RPC I/O in a wall-clock watchdog that kills and reaps the server on expiry and includes captured stderr in its method-specific TimeoutError | R:code:scripts/blackbox/mcp_conformance.py,code:scripts/blackbox/mcp_scenarios.py,code:scripts/blackbox/mcp_upgrade.py | A:rpc_deadline | S:A clock check around readline cannot interrupt a silent or partial line and select does not cover Windows pipes, so expiry kills the server; an expired session is unusable by design stdio_capture.py[CG5T]: F:Drains an MCP test server's stderr on a daemon thread, retains a bounded 16 KiB tail, and appends it to early-EOF diagnostics | R:code:scripts/blackbox/mcp_conformance.py,code:scripts/blackbox/mcp_scenarios.py,code:scripts/blackbox/mcp_upgrade.py | A:BoundedStderr,stderr_failure | S:An unread stderr pipe fills the OS buffer and blocks both sides; failure joins briefly and tolerates a session without a collector so bare test construction stays valid diff --git a/docs/cognition-volumes.md b/docs/cognition-volumes.md index 2b68556..58b9c76 100644 --- a/docs/cognition-volumes.md +++ b/docs/cognition-volumes.md @@ -54,9 +54,16 @@ The four responsibilities are deliberately singular: | `aoci.code.txt` | Code sections and model-authored file Entries | Project overview and copied Meta rules | | `aoci.database.txt` | Database namespace sections and one model-authored Entry per table | Schema/column/constraint sub-Entries and copied Meta rules | -## Historical Code roots and clones +## Code roots and clones -An absolute path in an `aoci.code.txt` directory section header is the +New `aoci init` repositories start their Code Volume with +`===project/.code/===`. New directory sections then use coordinates such as +`===/.code/src/===`, without including anyone's machine path. These are index +coordinates, not directories to create or read on disk. The root marker stays +even if every Entry is removed, so the next update keeps the same convention. +Existing indexes retain their historical section roots and need no migration. + +An absolute path in an older `aoci.code.txt` directory section header is the historical structural coordinate captured when that section family was created. It is not the active repository location, a path used to read source, or part of a Code object's semantic identity. The runtime repository root is diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 1bf2785..bafb476 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -17,6 +17,10 @@ host MCP integration to load the replaced binary. ## Section roots show an old absolute path +New `aoci init` repositories use the neutral root `===project/.code/===` and +section paths such as `/.code/src/`. These are coordinates inside the index, +not paths on your machine. Existing indexes keep their original roots. + Code Volume section headers such as `===/old/machine/path/project/===` are historical structural coordinates, not runtime paths. The public index format defines them that way: after a clone or relocation the formal bytes are @@ -24,14 +28,14 @@ preserved, and every reader derives repository-relative identities from the invocation root, never from the recorded prefix. An outdated prefix is expected, harmless, and not worth a formal write to rewrite. -A repository whose own path holds a space, `=`, `(`, or `(` shows two spellings +An older index created at a path holding a space, `=`, `(`, or `(` shows two spellings of that prefix: the full one in its root section and a truncated one in every -later section. Every release writes it that way, because the original reading +later section. Historical writers used that shape because the original reading of a header stops at that character and later sections continue what was read back. Both resolve to the same repository root, at the origin and in a checkout elsewhere; do not rewrite the headers by hand to make them match. -An index authored inside a git worktree nested under the primary checkout, +An older index authored inside a git worktree nested under the primary checkout, such as `/.worktrees/wt`, records that worktree as its root. Once the branch is merged, the primary checkout reads the same bytes with the recorded root below its own; the reader recognises the family and resolves every Entry @@ -63,7 +67,8 @@ Two things clear it, and then `aoci_maintain` issues the batch again: and a `curation_exclude` entry names a file, never a directory. `code_root_unspellable` is the same stop for the repository root itself, which -happens only when no part of the root path reads back as a usable root: a +can occur when establishing an unmarked index and no part of the root path +reads back as a usable root: a repository directory directly under `/` or a drive root (or under nothing but such segments) whose name begins with `(`, `(`, `=`, or whitespace. No scope rule helps there; move or rename the repository directory. A root whose first @@ -72,6 +77,10 @@ or trailing whitespace, is not refused: the index records the part of the path that a header can carry, and resolves the same way at the origin and in every checkout. +New Volume-first repositories use the neutral root, so the runtime repository +name cannot cause this root-level stop. Directory names inside the repository +still have to satisfy the section-header grammar. + ## Host config points to a moved binary or repository After the `aoci` binary or the repository moves, host configs written by diff --git a/docs/upgrading.md b/docs/upgrading.md index c165f63..6cbd1b4 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -36,6 +36,20 @@ aoci scope activate Where both semantics assigned the same roles the plan is identity-only: no role changes, no Entry changes, `aoci.txt` byte-identical, and policy-bound auto can authorize it without a human. Where a rule and a path genuinely differ only in case, the plan carries that real role change and is authorized as one. +## Neutral Code roots for new repositories + +New Volume-first repositories use `===project/.code/===` to avoid recording a +personal machine path in the Code Volume. Existing indexes keep their bytes +and historical roots; upgrading or running `init` again does not migrate them. +An interrupted older `init` can finish with its exact existing empty Code +skeleton, preserving that operation's original postimage. + +The marker uses the existing section grammar. Older binaries can read it in +ordinary checkout locations, but do not implement neutral-coordinate semantics: +a checkout path overlapping `/.code` may change path resolution, and deleting +the last Entry can remove the marker. Keep hosts on a binary implementing the +neutral-root rule when relying on those guarantees. + ## Directory and file names the index could not spell From v0.1.0-rc14 a section header carries a directory whose name holds a space, @@ -49,10 +63,10 @@ directory) may still resolve differently in a copy, as it always could. (An index that never aligned because an earlier release filed root files under a directory section, the first file it met having lived in a directory whose name begins with `(`, keeps that release's reading until the ordinary orphan repair -runs, which now completes.) Under a root whose own path holds one of those +runs, which now completes.) In an unmarked index under a root whose own path holds one of those characters, the root section spells the root in full and every later section -continues it as the original reading reads it back. Every release has written -that shape, this one included, so the two spellings of the root are deliberate; +continues it as the original reading reads it back. Historical writers produced +that shape, so the two spellings of the root are deliberate; do not edit the headers to make them match. The reverse direction is not supported for a repository that *uses* such a diff --git a/internal/cli/init_neutral_root_test.go b/internal/cli/init_neutral_root_test.go new file mode 100644 index 0000000..6b102e4 --- /dev/null +++ b/internal/cli/init_neutral_root_test.go @@ -0,0 +1,67 @@ +package cli + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/aoci-spec/aoci-code/internal/baseline" + "github.com/aoci-spec/aoci-code/internal/index" +) + +func TestInitCreatesNeutralCodeRootAndPreservesExistingCode(t *testing.T) { + root := t.TempDir() + if _, err := runInit(t, root, "--agent=", "--hooks=false"); err != nil { + t.Fatal(err) + } + codePath := filepath.Join(root, "aoci.code.txt") + raw, err := os.ReadFile(codePath) + if err != nil { + t.Fatal(err) + } + if string(raw) != "#AOCI-CODE-VOLUME: 1\n===project/.code/===\n" { + t.Fatalf("fresh Code skeleton must have a machine-independent root: %s", raw) + } + line := "a.go[EG7T]: F:Provides a source fixture | R:- | A:- | S:-" + written, err := index.InsertEntry(string(raw), "src/a.go", line, root) + if err != nil || strings.Contains(written, filepath.ToSlash(root)) { + t.Fatalf("first Entry must not record the machine path: %v\n%s", err, written) + } + for _, original := range []string{written, "#AOCI-CODE-VOLUME: 1\n===/old/machine/repo/===\n" + line + "\n"} { + if err := os.WriteFile(codePath, []byte(original), 0o644); err != nil { + t.Fatal(err) + } + if _, err := runInit(t, root, "--agent=", "--hooks=false"); err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(codePath) + if err != nil || string(got) != original { + t.Fatalf("init must preserve an established Code Volume: %v\n%s", err, got) + } + } +} + +func TestInitResumesOlderEmptyCodeSkeleton(t *testing.T) { + root := t.TempDir() + legacy := []byte("#AOCI-CODE-VOLUME: 1\n") + codePath := filepath.Join(root, "aoci.code.txt") + if err := os.WriteFile(codePath, legacy, 0o644); err != nil { + t.Fatal(err) + } + assets, err := renderInitialVolumeAssets(root) + if err != nil { + t.Fatal(err) + } + postimages, err := initializeVolumeFirst(root, "aoci.txt", assets) + if err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(codePath) + if err != nil || string(got) != string(legacy) { + t.Fatalf("older partial init must keep its exact Code postimage: %v\n%s", err, got) + } + if postimages["aoci.code.txt"] != baseline.HashBytes("aoci.code.txt", legacy) { + t.Fatal("Baseline must bind the preserved older Code skeleton") + } +} diff --git a/internal/cli/init_volumes.go b/internal/cli/init_volumes.go index b5c964d..81b7fd3 100644 --- a/internal/cli/init_volumes.go +++ b/internal/cli/init_volumes.go @@ -11,6 +11,7 @@ import ( "github.com/aoci-spec/aoci-code/internal/cognition" afs "github.com/aoci-spec/aoci-code/internal/fs" "github.com/aoci-spec/aoci-code/internal/hooks" + "github.com/aoci-spec/aoci-code/internal/index" "github.com/aoci-spec/aoci-code/textassets" ) @@ -44,7 +45,7 @@ func renderInitialVolumeAssets(root string) (initialVolumeAssets, error) { return initialVolumeAssets{ Root: []byte(rootText), Meta: []byte(metaText), - Code: []byte(cognition.CodeVolumeMarker + "\n"), + Code: []byte(cognition.CodeVolumeMarker + "\n" + index.NeutralCodeRootHeader + "\n"), }, nil } @@ -55,6 +56,16 @@ func initializeVolumeFirst(root, indexPath string, assets initialVolumeAssets) ( if filepath.ToSlash(indexPath) != "aoci.txt" { return nil, fmt.Errorf("init_volume_root_path_invalid") } + // An init interrupted before Root activation by an older binary may have + // already published its empty Code skeleton. Complete that exact postimage + // without rewriting it, and bind the bytes actually retained in the Baseline. + legacyCode := []byte(cognition.CodeVolumeMarker + "\n") + codePath := filepath.Join(root, "aoci.code.txt") + if info, err := os.Lstat(codePath); err == nil && info.Mode().IsRegular() && info.Size() == int64(len(legacyCode)) { + if raw, err := os.ReadFile(codePath); err == nil && bytes.Equal(raw, legacyCode) { + assets.Code = raw + } + } targets := []struct { rel string data []byte diff --git a/internal/index/editor_remove.go b/internal/index/editor_remove.go index d07fc8f..123a034 100644 --- a/internal/index/editor_remove.go +++ b/internal/index/editor_remove.go @@ -135,6 +135,11 @@ func PruneEmptySections(text string) string { if section.AbsPath == "" || len(section.Entries) != 0 { continue } + // Keep the neutral coordinate choice even after the last Entry is removed; + // otherwise the next insertion would start again from the runtime root. + if section == firstDirectorySection(document) && isNeutralCodeRoot(section) { + continue + } if anchorsOtherSections(document, section) { continue } diff --git a/internal/index/neutral_root.go b/internal/index/neutral_root.go new file mode 100644 index 0000000..b6996cf --- /dev/null +++ b/internal/index/neutral_root.go @@ -0,0 +1,33 @@ +package index + +// NeutralCodeRootHeader anchors new Code volumes without recording the host's +// repository path. The description distinguishes it from historical coordinates. +const NeutralCodeRootHeader = "===project/.code/===" + +const neutralCodeRoot = "/.code" + +func isNeutralCodeRoot(section *Section) bool { + return section != nil && section.HeaderLine == NeutralCodeRootHeader +} + +// neutralSectionReadings recognizes only the explicitly marked family. It must +// precede runtime matching: a checkout at /.code/src still has a src directory. +// A marked family with an outside section fails closed, never falls back to a +// mixture of neutral coordinates and runtime paths. +func neutralSectionReadings(doc *Document) (map[*Section]sectionReading, bool) { + if !isNeutralCodeRoot(firstDirectorySection(doc)) { + return nil, false + } + readings := make(map[*Section]sectionReading) + for _, section := range doc.Sections { + if section.AbsPath == "" { + continue + } + rel, ok := relUnder(normalizeRootPath(section.AbsPath), neutralCodeRoot) + if !ok { + return nil, true + } + readings[section] = sectionReading{rel: rel} + } + return readings, true +} diff --git a/internal/index/neutral_root_test.go b/internal/index/neutral_root_test.go new file mode 100644 index 0000000..8dcf03b --- /dev/null +++ b/internal/index/neutral_root_test.go @@ -0,0 +1,82 @@ +package index + +import ( + "reflect" + "strings" + "testing" +) + +func TestNeutralCodeRootSurvivesRelocationAndEdits(t *testing.T) { + const anchor = "===project/.code/===" + const line = "a.go[CG5T]: F:Provides a source fixture | R:- | A:- | S:-" + for _, root := range []string{"/home/alice/repo", "/home/alice/repo/.worktrees/wt", "C:/Users/Alice/repo", "/.code", "/.code/src", "/"} { + t.Run(root, func(t *testing.T) { + text := "#AOCI-CODE-VOLUME: 1\n" + anchor + "\n" + for _, rel := range []string{"src/a.go", "a.go", "app/(home)/a.go"} { + if err := CheckInsertable(text, rel, root); err != nil { + t.Fatalf("probe %s: %v", rel, err) + } + var err error + text, err = InsertEntry(text, rel, line, root) + if err != nil { + t.Fatalf("insert %s: %v", rel, err) + } + } + want := []string{"a.go", "app/(home)/a.go", "src/a.go"} + for _, checkout := range []string{root, "/srv/clone", "D:/clone", "/.code/src", "/"} { + if got := resolvedRelPaths(t, text, checkout); !reflect.DeepEqual(got, want) { + t.Fatalf("read at %s: got %v, want %v", checkout, got, want) + } + } + updated := strings.Replace(line, "source fixture", "updated fixture", 1) + text, err := ReplaceEntryForPath(text, root, "src/a.go", line, updated) + if err != nil { + t.Fatal(err) + } + doc := buildDoc(t, text) + ResolveRelPaths(doc, root) + if FindEntry(doc, "a.go").FullLine != line || FindEntry(doc, "src/a.go").FullLine != updated { + t.Fatal("same-named Entries must be updated by their relative paths") + } + for _, rel := range []string{"src/a.go", "a.go", "app/(home)/a.go"} { + old := line + if rel == "src/a.go" { + old = updated + } + text, err = RemoveEntryForPath(text, root, rel, old) + if err != nil { + t.Fatal(err) + } + } + if !strings.Contains(text, anchor) { + t.Fatal("removing the last Entry must retain the neutral coordinate anchor") + } + text, err = InsertEntry(text, "src/a.go", line, root) + if err != nil || !strings.Contains(text, "===/.code/src/===") { + t.Fatalf("reinsertion must keep neutral coordinates: %v\n%s", err, text) + } + }) + } +} + +func TestNeutralCodeRootDoesNotReinterpretHistoricalSections(t *testing.T) { + const line = "a.go[CG5T]: F:Provides a source fixture | R:- | A:- | S:-\n" + text := "===/.code/===\n" + line + "===/.code/src/===\n" + line + doc := buildDoc(t, text) + ResolveRelPaths(doc, "/.code/src") + if got := doc.Sections[1].Entries[0].RelPath; got != "a.go" { + t.Fatalf("unmarked historical sections retain direct matching: %s", got) + } +} + +func TestNeutralCodeRootRejectsAnOutsideSection(t *testing.T) { + text := "===project/.code/===\n===/other/src/===\na.go[CG5T]: F:Provides a source fixture | R:- | A:- | S:-\n" + doc := buildDoc(t, text) + ResolveRelPaths(doc, "/other") + if got := doc.Sections[1].Entries[0].RelPath; got != "" { + t.Fatalf("a marked family with an outside section must stay unresolved: %s", got) + } + if _, err := InsertEntry(text, "lib/a.go", "a.go[CG5T]: F:Provides a source fixture | R:- | A:- | S:-", "/other"); err == nil { + t.Fatal("must refuse to append to an inconsistent neutral family") + } +} diff --git a/internal/index/parser.go b/internal/index/parser.go index b943810..cc8872e 100644 --- a/internal/index/parser.go +++ b/internal/index/parser.go @@ -400,6 +400,9 @@ func (r sectionReading) path(sec *Section) string { } func resolveSectionReadings(doc *Document, repoRoot string) map[*Section]sectionReading { + if readings, neutral := neutralSectionReadings(doc); neutral { + return readings + } root := normalizeRootPath(repoRoot) first := firstDirectorySection(doc) var directories []*Section diff --git a/scripts/blackbox/mcp_scenarios.py b/scripts/blackbox/mcp_scenarios.py index a1567db..b88d9e9 100644 --- a/scripts/blackbox/mcp_scenarios.py +++ b/scripts/blackbox/mcp_scenarios.py @@ -1175,7 +1175,7 @@ def overview_meta_and_full_body(text): def parse_body_entries(body, fx): """Ordinal-ordered (rel_path, tag, core_f) parsed from a delivered body. - Section roots are historical coordinates; on Windows the fixture path is + Fresh fixtures use neutral coordinates. For historical roots, the fixture path is backslashed while the index writes forward slashes, so both sides are normalized before the prefix strip — os.path.relpath is unreliable across mixed separators.""" @@ -1183,6 +1183,9 @@ def parse_body_entries(body, fx): fx_norm = fx.replace("\\", "/").rstrip("/") for line in (body or "").splitlines(): line = line.rstrip("\r") + if line == "===project/.code/===": + fx_norm, section = "/.code", "" + continue if line.startswith("===") and line.endswith("==="): raw = line.strip("=").replace("\\", "/") rel = raw[len(fx_norm):].strip("/") if raw.startswith(fx_norm) else raw.strip("/") @@ -1654,25 +1657,15 @@ def group_p_special_names(): verified = rc == 0 and bool((v.get("governance") or {}).get("governance_aligned") or v.get("governance_aligned")) with open(os.path.join(d, "aoci.code.txt"), encoding="utf-8") as fh: volume = fh.read() - # Directory names are written as they are, under the root the index uses: the - # root section carries the full root and later sections continue it as the - # original reading reads it back (up to the first space, "=" or "(" of the - # path, wherever the work directory lives), the one shape every release writes. + # Fresh indexes retain literal directory names under neutral coordinates, + # even when the fixture's machine path contains a space. root = d.replace("\\", "/") - cut = min(i for i in (root.find(c) for c in " \t=((") if i >= 0) # the fixture name holds a space - family = root[:cut].rstrip("/") - if family: - headers = (f"==={root}/===" in volume - and all(f"==={family}/{sub}/===" in volume for sub in ("src/deep dir", "app/(home)", "pkg/max=", "pages/docs"))) - else: - # The work directory's first segment begins with a cut character, so no header - # can carry this root and the index records what reads back instead. Alignment, - # resolution, and the copy below still judge the scenario; only the literal - # header spelling is not asserted there. - headers = all(f"/{sub}/===" in volume for sub in ("src/deep dir", "app/(home)", "pkg/max=", "pages/docs")) + headers = ("===project/.code/===" in volume and root not in volume + and all(f"===/.code/{sub}/===" in volume + for sub in ("src/deep dir", "app/(home)", "pkg/max=", "pages/docs"))) resolved = all(f"object_ref=code:{rel}]" in body for rel in sources) and "Not indexed" not in body # A clone or a CI checkout has the same bytes under another absolute root, and - # every header above records this one. The copy must verify aligned as well. + # the neutral headers must verify aligned there as well. moved = os.path.join(WORK, "fx-special-names-checkout") shutil.rmtree(moved, ignore_errors=True) shutil.copytree(d, moved) diff --git a/scripts/release/clean-room-smoke.sh b/scripts/release/clean-room-smoke.sh index 7356bb6..161c803 100755 --- a/scripts/release/clean-room-smoke.sh +++ b/scripts/release/clean-room-smoke.sh @@ -55,8 +55,9 @@ git -C "$repository" commit --quiet -m "initial fixture" grep -qx '#AOCI-ROOT-MANIFEST: 1' "$repository/aoci.txt" grep -qx '#AOCI-META-VOLUME: 1' "$repository/aoci.meta.txt" grep -qx '#AOCI-CODE-VOLUME: 1' "$repository/aoci.code.txt" -if [ "$(wc -l <"$repository/aoci.code.txt")" -ne 1 ]; then - echo "fresh Volume-first Code must start with zero Entries" >&2 +grep -qx '===project/.code/===' "$repository/aoci.code.txt" +if [ "$(wc -l <"$repository/aoci.code.txt")" -ne 2 ]; then + echo "fresh Volume-first Code must start with a neutral root and zero Entries" >&2 exit 1 fi if [ -e "$repository/aoci.database.txt" ]; then diff --git a/spec/public/aoci-index-format-v1.txt b/spec/public/aoci-index-format-v1.txt index 854be36..9c33900 100644 --- a/spec/public/aoci-index-format-v1.txt +++ b/spec/public/aoci-index-format-v1.txt @@ -26,8 +26,27 @@ defines layout selection and consistency rules. Code section roots and clone relocation --------------------------------------- +New Volume-first initialization writes `===project/.code/===` as the first +directory section of the empty Code Volume. This exact header marks a neutral +coordinate root; it contains no host or repository location. Descendant +sections use `/.code//`. Readers resolve the +entire marked family against `/.code` before consulting the runtime root, even +when a checkout itself is named `/.code` or `/.code/src`. A section outside +that family makes its paths unresolved. The marker is retained even after the +last Entry is removed, so later insertions keep the neutral coordinates. + +This uses the existing directory-header grammar and does not change object +identities, Volume declarations, or the layout version. Existing indexes are +not rewritten or marked retroactively; unmarked sections retain their +historical interpretation. An interrupted older init may complete with its +exact existing empty Code skeleton. Older binaries parse neutral sections in +ordinary checkout locations but do not recognize their explicit coordinate +semantics: a runtime path overlapping `/.code` may resolve differently, and +removing the last Entry may discard the anchor. Use a reader implementing this +rule to retain the neutral-root guarantees. + The absolute path token in a Legacy or Code Volume directory section header is -the structural coordinate captured when that section family was created. It is +otherwise the structural coordinate captured when that section family was created. It is historical text, not the runtime repository root, a source-access path, or a semantic object identity. The runtime repository root is resolved for each invocation; an explicit `--repo` binds it directly and takes precedence over @@ -97,9 +116,9 @@ root only while a section sits there; an index without one, which earlier writers could leave behind, may resolve differently in a copy than at its origin. -Under a root holding one of those characters, the root section spells the root +For an unmarked index under a root holding one of those characters, the root section spells the root in full and every later section continues it as the original reading reads it -back, that is, truncated at the character. Every release has written this one +back, that is, truncated at the character. Historical writers produced this shape, so a reader older than this rule still resolves every section of it whose own directory name it can read. When the repository root has no header that reads back to it at all (its first segment begins with `(`, `(`, or `=`,