@quolu/lattice 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/lattice.mjs CHANGED
@@ -73,6 +73,26 @@ if (sensorRelaunchStatus !== null) {
73
73
  } catch (error) {
74
74
  process.exitCode = projectCliFailure(process.stderr, error);
75
75
  }
76
+ } else if (args.length === 7 && args[0] === 'plan' && args[1] === 'scope-review'
77
+ && args[2] === '--plan-input' && args[4] === '--review' && args[6] === '--json') {
78
+ const { runPlanScopeReview } = await import('../src/plan-scope-review.mjs');
79
+ const { projectCliFailure } = await import('../src/project-cli.mjs');
80
+ try {
81
+ process.exitCode = await runPlanScopeReview({
82
+ cwd: process.cwd(), planInputRef: args[3], reviewRef: args[5], stdout: process.stdout,
83
+ });
84
+ } catch (error) {
85
+ process.exitCode = projectCliFailure(process.stderr, error);
86
+ }
87
+ } else if (args.length === 4 && args[0] === 'plan' && args[1] === 'scope-review'
88
+ && args[2] === '--schema' && args[3] === '--json') {
89
+ const { runPlanScopeReviewSchema } = await import('../src/plan-scope-review.mjs');
90
+ const { projectCliFailure } = await import('../src/project-cli.mjs');
91
+ try {
92
+ process.exitCode = await runPlanScopeReviewSchema({ stdout: process.stdout });
93
+ } catch (error) {
94
+ process.exitCode = projectCliFailure(process.stderr, error);
95
+ }
76
96
  } else if (args.length === 2 && args[0] === 'factory-diagnostics' && args[1] === '--json') {
77
97
  const { buildFactoryDiagnostics } = await import('../src/factory-diagnostics.mjs');
78
98
  const diagnostics = await buildFactoryDiagnostics();
@@ -39,9 +39,9 @@
39
39
  },
40
40
  "forbidden_operations": {
41
41
  "type": "array",
42
- "minItems": 1,
43
42
  "maxItems": 256,
44
- "items": { "type": "string" }
43
+ "items": { "type": "string" },
44
+ "description": "Compatibility field. Lattice-produced packets use an empty array; operation authority belongs to the host."
45
45
  },
46
46
  "context_content_digest": { "$ref": "#/$defs/digest" },
47
47
  "packet_digest": { "$ref": "#/$defs/digest" }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://github.com/kitepon-rgb/Lattice/blob/main/docs/schemas/lattice.plan_create_input.v4.schema.json",
4
4
  "title": "lattice.plan_create_input.v4",
5
- "description": "Canonical initial authoring input for a phase-audited plan. Every task requires a design memo. あなたがこのToDoに対して、何も考えていないならば、設計メモに `NO_PLAN` と書いてください",
5
+ "description": "Canonical initial authoring input for a phase-audited plan. Every task requires a design memo. After the complete plan is written, run `lattice plan scope-review` and confirm that every task preserves an owner-supplied work specification without adding a product purpose. あなたがこのToDoに対して、何も考えていないならば、設計メモに `NO_PLAN` と書いてください",
6
6
  "$comment": "Runtime validation additionally enforces canonical JSON+LF bytes, sorted identifiers and edges, unique ids, combined task/phase acyclicity, byte bounds, non-empty design_memo, and input_digest.",
7
7
  "type": "object",
8
8
  "additionalProperties": false,
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/kitepon-rgb/Lattice/blob/main/docs/schemas/lattice.plan_scope_review.v1.schema.json",
4
+ "title": "lattice.plan_scope_review.v1",
5
+ "description": "AI-authored review performed after the complete plan is written and before it is registered. Every planned task is judged against the owner-supplied work specifications; task splitting and parallelization are allowed, but a new product purpose is not.",
6
+ "$comment": "Lattice validates complete task coverage, known work-spec references, verdict consistency, and digest binding. The operating AI owns the semantic judgment.",
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "required": [
10
+ "schema", "authoring_digest", "work_specs", "task_assessments", "verdict", "review_digest"
11
+ ],
12
+ "properties": {
13
+ "schema": { "const": "lattice.plan_scope_review.v1" },
14
+ "authoring_digest": { "$ref": "#/$defs/digest" },
15
+ "work_specs": {
16
+ "type": "array", "minItems": 1, "maxItems": 512,
17
+ "description": "The owner-supplied work specifications. Do not add inferred improvements as work specifications.",
18
+ "items": { "$ref": "#/$defs/workSpec" }
19
+ },
20
+ "task_assessments": {
21
+ "type": "array", "minItems": 1, "maxItems": 512,
22
+ "description": "Exactly one assessment for every task that will be registered. Use out_of_scope when the task creates a purpose not required by any work specification.",
23
+ "items": { "$ref": "#/$defs/taskAssessment" }
24
+ },
25
+ "verdict": { "enum": ["scope_preserved", "scope_mismatch"] },
26
+ "review_digest": { "$ref": "#/$defs/digest" }
27
+ },
28
+ "$defs": {
29
+ "identifier": { "type": "string", "pattern": "^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$" },
30
+ "digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
31
+ "text": { "type": "string", "minLength": 1, "maxLength": 16384, "pattern": "\\S" },
32
+ "workSpec": {
33
+ "type": "object", "additionalProperties": false,
34
+ "required": ["work_spec_id", "requirement", "acceptance"],
35
+ "properties": {
36
+ "work_spec_id": { "$ref": "#/$defs/identifier" },
37
+ "requirement": { "$ref": "#/$defs/text" },
38
+ "acceptance": { "$ref": "#/$defs/text" }
39
+ }
40
+ },
41
+ "taskAssessment": {
42
+ "type": "object", "additionalProperties": false,
43
+ "required": ["task_id", "work_spec_ids", "judgment", "reason"],
44
+ "properties": {
45
+ "task_id": { "$ref": "#/$defs/identifier" },
46
+ "work_spec_ids": {
47
+ "type": "array", "maxItems": 512, "uniqueItems": true,
48
+ "items": { "$ref": "#/$defs/identifier" }
49
+ },
50
+ "judgment": { "enum": ["required", "out_of_scope"] },
51
+ "reason": { "$ref": "#/$defs/text" }
52
+ }
53
+ }
54
+ }
55
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://github.com/kitepon-rgb/Lattice/blob/main/docs/schemas/lattice.todo_extraction.v3.schema.json",
4
4
  "title": "lattice.todo_extraction.v3",
5
- "description": "AI-authored ToDo migration artifact. Every task carries a non-blank Markdown design_memo. If no plan exists, write NO_PLAN explicitly.",
5
+ "description": "AI-authored ToDo migration artifact. Every task carries a non-blank Markdown design_memo. After the complete plan is written, run `lattice plan scope-review` and confirm that every registered task preserves an owner-supplied work specification without adding a product purpose. If no plan exists, write NO_PLAN explicitly.",
6
6
  "$comment": "あなたがこのToDoに対して、何も考えていないならば、設計メモに `NO_PLAN` と書いてください。Runtime validation also enforces UTF-8 byte bounds, exact keys, strict sort order, referential integrity, disposition consistency, and the canonical self-digest.",
7
7
  "type": "object",
8
8
  "additionalProperties": false,
@@ -22,7 +22,11 @@
22
22
  "type": "array", "maxItems": 2048,
23
23
  "items": {
24
24
  "type": "object", "additionalProperties": false, "required": ["from", "to"],
25
- "properties": { "from": { "$ref": "#/$defs/nodeRef" }, "to": { "$ref": "#/$defs/nodeRef" } }
25
+ "properties": {
26
+ "from": { "$ref": "#/$defs/nodeRef" },
27
+ "to": { "$ref": "#/$defs/nodeRef" },
28
+ "reason": { "$ref": "#/$defs/text" }
29
+ }
26
30
  }
27
31
  },
28
32
  "joins": {
@@ -0,0 +1,161 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/kitepon-rgb/Lattice/blob/main/docs/schemas/lattice.todo_extraction.v4.schema.json",
4
+ "title": "lattice.todo_extraction.v4",
5
+ "description": "AI-authored ToDo migration artifact. Every task carries a non-blank Markdown design_memo. After the complete plan is written, run `lattice plan scope-review` and confirm that every registered task preserves an owner-supplied work specification without adding a product purpose. If no plan exists, write NO_PLAN explicitly. Runtime validation accepts at most one explicit, reasoned cross-plan dependency; an empty tasks array is reserved for that one dependency between existing tasks, with the top-level plan identity naming the source plan and both endpoint topology digests.",
6
+ "$comment": "あなたがこのToDoに対して、何も考えていないならば、設計メモに `NO_PLAN` と書いてください。Runtime validation also enforces UTF-8 byte bounds, exact keys, strict sort order, referential integrity, disposition consistency, the cross-plan dependency limit, and the canonical self-digest.",
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "required": [
10
+ "schema", "project_id", "plan_key", "plan_version", "actor", "recorded_at",
11
+ "tasks", "hard_dependencies", "joins", "extraction_digest"
12
+ ],
13
+ "properties": {
14
+ "schema": { "const": "lattice.todo_extraction.v4" },
15
+ "project_id": { "$ref": "#/$defs/identifier" },
16
+ "plan_key": { "$ref": "#/$defs/identifier" },
17
+ "plan_version": { "$ref": "#/$defs/identifier" },
18
+ "actor": { "$ref": "#/$defs/actor" },
19
+ "recorded_at": { "$ref": "#/$defs/strictTimestamp" },
20
+ "tasks": { "type": "array", "minItems": 0, "maxItems": 512, "items": { "$ref": "#/$defs/task" } },
21
+ "hard_dependencies": {
22
+ "type": "array", "maxItems": 2048,
23
+ "items": {
24
+ "type": "object", "additionalProperties": false, "required": ["from", "to"],
25
+ "properties": {
26
+ "from": { "$ref": "#/$defs/nodeRef" },
27
+ "to": { "$ref": "#/$defs/nodeRef" },
28
+ "reason": { "$ref": "#/$defs/text" }
29
+ }
30
+ }
31
+ },
32
+ "joins": {
33
+ "type": "array", "maxItems": 128,
34
+ "items": {
35
+ "type": "object", "additionalProperties": false, "required": ["id", "after", "before"],
36
+ "properties": {
37
+ "id": { "$ref": "#/$defs/identifier" },
38
+ "after": { "type": "array", "minItems": 1, "maxItems": 512, "items": { "$ref": "#/$defs/nodeRef" } },
39
+ "before": { "$ref": "#/$defs/nodeRef" }
40
+ }
41
+ }
42
+ },
43
+ "extraction_digest": { "$ref": "#/$defs/digest" }
44
+ },
45
+ "allOf": [
46
+ {
47
+ "if": { "properties": { "tasks": { "maxItems": 0 } }, "required": ["tasks"] },
48
+ "then": {
49
+ "properties": {
50
+ "hard_dependencies": { "minItems": 1, "maxItems": 1 },
51
+ "joins": { "maxItems": 0 }
52
+ }
53
+ }
54
+ }
55
+ ],
56
+ "$defs": {
57
+ "identifier": { "type": "string", "pattern": "^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$" },
58
+ "digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
59
+ "repoRef": {
60
+ "type": "string", "minLength": 1, "maxLength": 1024,
61
+ "pattern": "^(?!/)(?!.*(?:^|/)\\.{1,2}(?:/|$))(?!.*\\\\)(?!.*[\\u0000-\\u001f\\u007f])[^/]+(?:/[^/]+)*$"
62
+ },
63
+ "text": { "type": "string", "minLength": 1, "maxLength": 16384 },
64
+ "designMemo": {
65
+ "type": "string", "minLength": 1, "maxLength": 16384, "pattern": "\\S",
66
+ "description": "Markdown design memo. Use the literal NO_PLAN only when no plan exists."
67
+ },
68
+ "nullableText": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/text" }] },
69
+ "strictTimestamp": {
70
+ "type": "string",
71
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$"
72
+ },
73
+ "actor": {
74
+ "type": "object", "additionalProperties": false, "required": ["host", "session", "agent"],
75
+ "properties": {
76
+ "host": { "$ref": "#/$defs/identifier" },
77
+ "session": { "$ref": "#/$defs/identifier" },
78
+ "agent": { "$ref": "#/$defs/identifier" }
79
+ }
80
+ },
81
+ "nodeRef": {
82
+ "type": "object", "additionalProperties": false, "required": ["project_id", "plan_key", "task_id"],
83
+ "properties": {
84
+ "project_id": { "$ref": "#/$defs/identifier" },
85
+ "plan_key": { "$ref": "#/$defs/identifier" },
86
+ "task_id": { "$ref": "#/$defs/identifier" },
87
+ "expected_topology_digest": { "$ref": "#/$defs/digest" }
88
+ }
89
+ },
90
+ "sourceLocation": {
91
+ "type": "object", "additionalProperties": false,
92
+ "required": ["origin_plan_ref", "origin_line", "source_commit", "heading_path", "markdown_depth", "parent_task_id", "checkbox_state"],
93
+ "properties": {
94
+ "origin_plan_ref": { "$ref": "#/$defs/repoRef" },
95
+ "origin_line": { "type": "integer", "minimum": 1 },
96
+ "source_commit": { "type": "string", "pattern": "^(?:[0-9a-f]{40}|[0-9a-f]{64})$" },
97
+ "heading_path": { "type": "array", "maxItems": 32, "items": { "type": "string", "minLength": 1, "maxLength": 1024 } },
98
+ "markdown_depth": { "type": "integer", "minimum": 0, "maximum": 64 },
99
+ "parent_task_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] },
100
+ "checkbox_state": { "enum": ["checked", "unchecked", "absent", "ambiguous"] }
101
+ }
102
+ },
103
+ "migrationContext": {
104
+ "type": "object", "additionalProperties": false,
105
+ "required": ["external_canonical_ref", "carry_over_ref", "h_required", "condition", "evidence_refs", "notes"],
106
+ "properties": {
107
+ "external_canonical_ref": { "$ref": "#/$defs/nullableText" },
108
+ "carry_over_ref": { "$ref": "#/$defs/nullableText" },
109
+ "h_required": { "type": "boolean" },
110
+ "condition": { "$ref": "#/$defs/nullableText" },
111
+ "evidence_refs": { "type": "array", "maxItems": 64, "items": { "type": "string", "minLength": 1, "maxLength": 4096 } },
112
+ "notes": { "type": "array", "maxItems": 64, "items": { "type": "string", "minLength": 1, "maxLength": 4096 } }
113
+ }
114
+ },
115
+ "historicalStart": {
116
+ "type": "object", "additionalProperties": false, "required": ["start_mode", "status", "started_at"],
117
+ "properties": {
118
+ "start_mode": { "const": "historical_import" }, "status": { "const": "in-progress" },
119
+ "started_at": { "oneOf": [{ "const": "unknown_requires_evidence" }, { "$ref": "#/$defs/strictTimestamp" }] }
120
+ }
121
+ },
122
+ "historicalCompletion": {
123
+ "type": "object", "additionalProperties": false, "required": ["done_mode", "completed_at"],
124
+ "properties": {
125
+ "done_mode": { "const": "historical_import" },
126
+ "completed_at": { "oneOf": [{ "const": "unknown_requires_evidence" }, { "$ref": "#/$defs/strictTimestamp" }] }
127
+ }
128
+ },
129
+ "task": {
130
+ "type": "object", "additionalProperties": false,
131
+ "required": [
132
+ "task_id", "title", "lane", "design_memo", "narrative_ref", "compile_binding",
133
+ "disposition", "start", "completion", "source", "migration_context"
134
+ ],
135
+ "properties": {
136
+ "task_id": { "$ref": "#/$defs/identifier" },
137
+ "title": { "$ref": "#/$defs/text" },
138
+ "lane": { "$ref": "#/$defs/identifier" },
139
+ "design_memo": { "$ref": "#/$defs/designMemo" },
140
+ "narrative_ref": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/repoRef" }] },
141
+ "compile_binding": { "type": "null" },
142
+ "disposition": { "enum": ["register_pending", "register_in_progress", "register_done", "exclude_superseded", "exclude_compatibility_record", "unknown_requires_evidence"] },
143
+ "start": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/historicalStart" }] },
144
+ "completion": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/historicalCompletion" }] },
145
+ "source": { "$ref": "#/$defs/sourceLocation" },
146
+ "migration_context": { "$ref": "#/$defs/migrationContext" }
147
+ },
148
+ "allOf": [
149
+ {
150
+ "if": { "properties": { "disposition": { "const": "register_in_progress" } }, "required": ["disposition"] },
151
+ "then": { "properties": { "start": { "$ref": "#/$defs/historicalStart" }, "completion": { "type": "null" } } },
152
+ "else": {
153
+ "if": { "properties": { "disposition": { "const": "register_done" } }, "required": ["disposition"] },
154
+ "then": { "properties": { "start": { "type": "null" }, "completion": { "$ref": "#/$defs/historicalCompletion" } } },
155
+ "else": { "properties": { "start": { "type": "null" }, "completion": { "type": "null" } } }
156
+ }
157
+ }
158
+ ]
159
+ }
160
+ }
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
5
5
  "author": {
6
6
  "name": "Quo / クオ at kitepon.dev",
@@ -29,11 +29,13 @@
29
29
  "docs/schemas/lattice.plan_create_input.v2.schema.json",
30
30
  "docs/schemas/lattice.plan_create_input.v3.schema.json",
31
31
  "docs/schemas/lattice.plan_create_input.v4.schema.json",
32
+ "docs/schemas/lattice.plan_scope_review.v1.schema.json",
32
33
  "docs/schemas/lattice.todo_revision.v2.schema.json",
33
34
  "docs/schemas/lattice.todo_revision_set.v3.schema.json",
34
35
  "docs/schemas/lattice.phase_todo_revision.v3.schema.json",
35
36
  "docs/schemas/lattice.todo_extraction.v2.schema.json",
36
37
  "docs/schemas/lattice.todo_extraction.v3.schema.json",
38
+ "docs/schemas/lattice.todo_extraction.v4.schema.json",
37
39
  "docs/schemas/lattice.todo_structure_set.v1.schema.json",
38
40
  "docs/schemas/lattice.todo_structure_realization.v1.schema.json",
39
41
  "docs/schemas/lattice.todo_structure_binding.v1.schema.json",
package/src/cli-help.mjs CHANGED
@@ -91,7 +91,7 @@ Write commands:
91
91
  dashboard remove <project_id> --json # 登録簿から1件外す(対象repoの外からも叩ける)
92
92
  note --plan <key> [--task <id>] (--message <text>|--input <file>)
93
93
  # ToDoへ作業継続に必要な方針・調査結果・注意をappend-onlyで追記する
94
- migrate --input <extraction.json> [--serialization-reviewed]
94
+ migrate --input <extraction.json> [--serialization-reviewed] [--json]
95
95
  migrate --input <extraction.json> --dry-run --json [--serialization-reviewed]
96
96
  # 既存storeへplanを追加する(plan createは空store初期化専用)。
97
97
  # 依存グラフがほぼ一直線なら一度突き返し、再考後の --serialization-reviewed で通す
@@ -202,6 +202,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
202
202
  'session-context': 'session-context --json',
203
203
  'plan create': 'plan create --input <file> [--serialization-reviewed] | --schema --json | --schema-version <1|2|3|4> --json',
204
204
  'plan show': 'plan show <plan_key> --json',
205
+ 'plan scope-review': 'plan scope-review --plan-input <plan-create-or-extraction.json> --review <scope-review.json> --json | --schema --json',
205
206
  'plan compile': 'plan compile --request <request.json> | --schema --json',
206
207
  'plan verify': 'plan verify --request <request.json> --plan <plan.json>',
207
208
  'run start': 'run start --request <request.json> --executor <adapter>'
@@ -272,7 +273,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
272
273
  'todo revise': 'todo revise --plan <key> --input <file> | --schema --json',
273
274
  'todo revise-phase': 'todo revise-phase --plan <key> --input <file> | --schema --json',
274
275
  'todo revise-set': 'todo revise-set --input <file> | --schema --json',
275
- 'todo migrate': 'todo migrate --input <extraction.json> [--serialization-reviewed] | --input <extraction.json> --dry-run --json [--serialization-reviewed] | --schema --json',
276
+ 'todo migrate': 'todo migrate --input <extraction.json> [--serialization-reviewed] [--json] | --input <extraction.json> --dry-run --json [--serialization-reviewed] | --schema --json',
276
277
  'sensor init': 'sensor init [path] --json',
277
278
  'sensor sync': 'sensor sync [path] --json',
278
279
  'sensor diff': 'sensor diff <rootA> <rootB> [--subtree-a <rel>] [--subtree-b <rel>]'
@@ -0,0 +1,214 @@
1
+ import { constants as fsConstants } from 'node:fs';
2
+ import { lstat, open, realpath, readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ import { gitSync } from './git-process.mjs';
6
+ import {
7
+ exactRecord,
8
+ isTodoDigest,
9
+ isTodoIdentifier,
10
+ isTodoRef,
11
+ todoSelfDigest,
12
+ } from './todo-contracts.mjs';
13
+ import { TodoStoreError } from './todo-store.mjs';
14
+
15
+ const REVIEW_SCHEMA = 'lattice.plan_scope_review.v1';
16
+ const RESULT_SCHEMA = 'lattice.plan_scope_review_result.v1';
17
+ const PLAN_CREATE_SCHEMA = 'lattice.plan_create_input.v4';
18
+ const TODO_EXTRACTION_SCHEMAS = new Set([
19
+ 'lattice.todo_extraction.v3',
20
+ 'lattice.todo_extraction.v4',
21
+ ]);
22
+ const MAX_INPUT_BYTES = 8_388_608;
23
+ const JUDGMENTS = new Set(['required', 'out_of_scope']);
24
+
25
+ function boundedText(value) {
26
+ return typeof value === 'string' && value.trim().length > 0
27
+ && Buffer.byteLength(value) <= 16_384;
28
+ }
29
+
30
+ function sortedStrictly(values, key = (value) => value) {
31
+ return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value));
32
+ }
33
+
34
+ function within(root, candidate) {
35
+ return candidate.startsWith(`${root}${path.sep}`);
36
+ }
37
+
38
+ function resolveRepoRoot(cwd) {
39
+ try {
40
+ return path.resolve(gitSync(['rev-parse', '--show-toplevel'], {
41
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
42
+ }).replace(/\r?\n$/u, ''));
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ async function readJsonWithinRepo(repoRoot, inputRef) {
49
+ if (!isTodoRef(inputRef)) {
50
+ throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_outside_repo', undefined,
51
+ { input_ref: inputRef });
52
+ }
53
+ const canonicalRoot = await realpath(repoRoot);
54
+ const absolute = path.resolve(canonicalRoot, inputRef);
55
+ if (!within(canonicalRoot, absolute)) {
56
+ throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_outside_repo', undefined,
57
+ { input_ref: inputRef });
58
+ }
59
+ let stats;
60
+ try { stats = await lstat(absolute); } catch {
61
+ throw new TodoStoreError('INPUT_UNREADABLE', 'input_unreadable', undefined,
62
+ { input_ref: inputRef });
63
+ }
64
+ if (!stats.isFile() || stats.isSymbolicLink() || stats.size > MAX_INPUT_BYTES
65
+ || await realpath(absolute) !== absolute) {
66
+ throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_unsafe', undefined,
67
+ { input_ref: inputRef });
68
+ }
69
+ let value;
70
+ try { value = JSON.parse(await readFile(absolute, 'utf8')); } catch {
71
+ throw new TodoStoreError('INPUT_INVALID', 'input_json_invalid', undefined,
72
+ { input_ref: inputRef });
73
+ }
74
+ return value;
75
+ }
76
+
77
+ function authoredPlan(value) {
78
+ const digestField = value?.schema === PLAN_CREATE_SCHEMA ? 'input_digest'
79
+ : TODO_EXTRACTION_SCHEMAS.has(value?.schema) ? 'extraction_digest' : null;
80
+ if (digestField === null) {
81
+ throw new TodoStoreError('PLAN_SCOPE_REVIEW_INVALID', 'authoring_schema_unsupported', undefined, {
82
+ pointer: '/schema', expected: [PLAN_CREATE_SCHEMA, ...TODO_EXTRACTION_SCHEMAS],
83
+ actual: typeof value?.schema === 'string' ? value.schema : null,
84
+ });
85
+ }
86
+ if (!isTodoDigest(value[digestField])
87
+ || value[digestField] !== todoSelfDigest(value, digestField)) {
88
+ throw new TodoStoreError('PLAN_SCOPE_REVIEW_INVALID', 'authoring_digest_mismatch', undefined, {
89
+ pointer: `/${digestField}`,
90
+ });
91
+ }
92
+ const selected = TODO_EXTRACTION_SCHEMAS.has(value.schema)
93
+ ? value.tasks?.filter((task) => typeof task?.disposition === 'string'
94
+ && task.disposition.startsWith('register_'))
95
+ : value.tasks;
96
+ if (!Array.isArray(selected) || selected.length === 0
97
+ || selected.some((task) => !isTodoIdentifier(task?.task_id))) {
98
+ throw new TodoStoreError('PLAN_SCOPE_REVIEW_INVALID', 'authoring_tasks_invalid', undefined, {
99
+ pointer: '/tasks',
100
+ });
101
+ }
102
+ const taskIds = selected.map(({ task_id: taskId }) => taskId).sort();
103
+ if (new Set(taskIds).size !== taskIds.length) {
104
+ throw new TodoStoreError('PLAN_SCOPE_REVIEW_INVALID', 'authoring_task_ids_duplicate', undefined, {
105
+ pointer: '/tasks',
106
+ });
107
+ }
108
+ return { schema: value.schema, digest: value[digestField], taskIds };
109
+ }
110
+
111
+ function invalid(reason, pointer, detail = {}) {
112
+ throw new TodoStoreError('PLAN_SCOPE_REVIEW_INVALID', reason, undefined, { pointer, ...detail });
113
+ }
114
+
115
+ function validateReview(review, authored) {
116
+ if (!exactRecord(review, [
117
+ 'schema', 'authoring_digest', 'work_specs', 'task_assessments', 'verdict', 'review_digest',
118
+ ]) || review.schema !== REVIEW_SCHEMA) invalid('review_schema_invalid', '/schema');
119
+ if (!isTodoDigest(review.authoring_digest) || review.authoring_digest !== authored.digest) {
120
+ invalid('review_authoring_digest_mismatch', '/authoring_digest', {
121
+ expected: authored.digest, actual: review.authoring_digest ?? null,
122
+ });
123
+ }
124
+ if (!isTodoDigest(review.review_digest)
125
+ || review.review_digest !== todoSelfDigest(review, 'review_digest')) {
126
+ invalid('review_digest_mismatch', '/review_digest');
127
+ }
128
+ if (!Array.isArray(review.work_specs) || review.work_specs.length === 0
129
+ || !review.work_specs.every((spec) => exactRecord(spec, [
130
+ 'work_spec_id', 'requirement', 'acceptance',
131
+ ]) && isTodoIdentifier(spec.work_spec_id) && boundedText(spec.requirement)
132
+ && boundedText(spec.acceptance))
133
+ || !sortedStrictly(review.work_specs, ({ work_spec_id: id }) => id)) {
134
+ invalid('work_specs_invalid', '/work_specs');
135
+ }
136
+ const workSpecIds = new Set(review.work_specs.map(({ work_spec_id: id }) => id));
137
+ if (!Array.isArray(review.task_assessments)
138
+ || !review.task_assessments.every((assessment) => exactRecord(assessment, [
139
+ 'task_id', 'work_spec_ids', 'judgment', 'reason',
140
+ ]) && isTodoIdentifier(assessment.task_id) && Array.isArray(assessment.work_spec_ids)
141
+ && assessment.work_spec_ids.every(isTodoIdentifier)
142
+ && sortedStrictly(assessment.work_spec_ids) && JUDGMENTS.has(assessment.judgment)
143
+ && boundedText(assessment.reason)
144
+ && (assessment.judgment === 'required'
145
+ ? assessment.work_spec_ids.length > 0 : assessment.work_spec_ids.length === 0)
146
+ && assessment.work_spec_ids.every((id) => workSpecIds.has(id)))
147
+ || !sortedStrictly(review.task_assessments, ({ task_id: id }) => id)) {
148
+ invalid('task_assessments_invalid', '/task_assessments');
149
+ }
150
+ const assessedTaskIds = review.task_assessments.map(({ task_id: id }) => id);
151
+ if (assessedTaskIds.length !== authored.taskIds.length
152
+ || assessedTaskIds.some((id, index) => id !== authored.taskIds[index])) {
153
+ invalid('task_assessments_incomplete', '/task_assessments', {
154
+ expected_task_ids: authored.taskIds, actual_task_ids: assessedTaskIds,
155
+ });
156
+ }
157
+ const outOfScopeTaskIds = review.task_assessments
158
+ .filter(({ judgment }) => judgment === 'out_of_scope')
159
+ .map(({ task_id: id }) => id);
160
+ const covered = new Set(review.task_assessments.flatMap(({ work_spec_ids: ids }) => ids));
161
+ const uncoveredWorkSpecIds = [...workSpecIds].filter((id) => !covered.has(id));
162
+ const expectedVerdict = outOfScopeTaskIds.length === 0 && uncoveredWorkSpecIds.length === 0
163
+ ? 'scope_preserved' : 'scope_mismatch';
164
+ if (review.verdict !== expectedVerdict) {
165
+ invalid('review_verdict_inconsistent', '/verdict', {
166
+ expected: expectedVerdict, actual: review.verdict ?? null,
167
+ });
168
+ }
169
+ return { outOfScopeTaskIds, uncoveredWorkSpecIds };
170
+ }
171
+
172
+ export function evaluatePlanScopeReview(planInput, review) {
173
+ const authored = authoredPlan(planInput);
174
+ const findings = validateReview(review, authored);
175
+ const result = {
176
+ schema: RESULT_SCHEMA,
177
+ authoring_schema: authored.schema,
178
+ authoring_digest: authored.digest,
179
+ work_spec_count: review.work_specs.length,
180
+ reviewed_task_count: review.task_assessments.length,
181
+ verdict: review.verdict,
182
+ accepted: review.verdict === 'scope_preserved',
183
+ out_of_scope_task_ids: findings.outOfScopeTaskIds,
184
+ uncovered_work_spec_ids: findings.uncoveredWorkSpecIds,
185
+ review_digest: review.review_digest,
186
+ result_digest: '',
187
+ };
188
+ result.result_digest = todoSelfDigest(result, 'result_digest');
189
+ return result;
190
+ }
191
+
192
+ export async function runPlanScopeReview({ cwd, planInputRef, reviewRef, stdout }) {
193
+ const repoRoot = resolveRepoRoot(cwd);
194
+ if (repoRoot === null) throw new TodoStoreError('REPO_UNRESOLVED', 'git_toplevel_unresolved');
195
+ const [planInput, review] = await Promise.all([
196
+ readJsonWithinRepo(repoRoot, planInputRef), readJsonWithinRepo(repoRoot, reviewRef),
197
+ ]);
198
+ const result = evaluatePlanScopeReview(planInput, review);
199
+ stdout.write(`${JSON.stringify(result)}\n`);
200
+ return result.accepted ? 0 : 1;
201
+ }
202
+
203
+ export async function runPlanScopeReviewSchema({ stdout }) {
204
+ const schemaUrl = new URL('../docs/schemas/lattice.plan_scope_review.v1.schema.json', import.meta.url);
205
+ const handle = await open(schemaUrl, fsConstants.O_RDONLY);
206
+ try {
207
+ const schema = JSON.parse(await handle.readFile('utf8'));
208
+ if (schema?.title !== REVIEW_SCHEMA) throw new TypeError('bundled plan scope review schema invalid');
209
+ stdout.write(`${JSON.stringify(schema)}\n`);
210
+ return 0;
211
+ } finally {
212
+ await handle.close();
213
+ }
214
+ }
@@ -19,7 +19,10 @@ import { projectTodoStatus } from './todo-status.mjs';
19
19
  import { readTodoPlanNotesForStatus } from './todo-note-store.mjs';
20
20
  import { projectIndependenceFrontier } from './todo-independence.mjs';
21
21
  import { readTodoParallelCandidatesForStatus } from './todo-parallel-candidates.mjs';
22
- import { readTodoStructureFinalizationsForStatus } from './todo-structure-store.mjs';
22
+ import {
23
+ readTodoStructureArtifactDiagnostics,
24
+ readTodoStructureFinalizationsForStatus,
25
+ } from './todo-structure-store.mjs';
23
26
  import { isTodoIndependenceLegacyMarker } from './todo-independence-contracts.mjs';
24
27
  import { selectIndependenceGuidance } from './todo-independence-guidance.mjs';
25
28
  import { ensureTodoDashboardActivity } from './todo-dashboard-registry.mjs';
@@ -223,7 +226,7 @@ function invalidStatus({ cliVersion, repoRoot, reason, nextAction = null }) {
223
226
  * store読みはここでしか行わない——session開始経路が同じstoreを二度払っていたのが
224
227
  * ADR 0131で直した欠陥である。
225
228
  */
226
- async function resolveProjectState({ cwd, cliVersion }) {
229
+ async function resolveProjectState({ cwd, cliVersion, diagnoseStructureArtifacts = false }) {
227
230
  const repoRoot = resolveRepoRoot(cwd);
228
231
  if (repoRoot === null) {
229
232
  return { exitCode: 1, repoRoot: null, store: null, todo: null,
@@ -271,6 +274,14 @@ async function resolveProjectState({ cwd, cliVersion }) {
271
274
  }
272
275
  try {
273
276
  const store = await readTodoStore({ repoRoot });
277
+ if (diagnoseStructureArtifacts) {
278
+ const diagnostics = await readTodoStructureArtifactDiagnostics({ repoRoot, store });
279
+ if (diagnostics.length > 0) {
280
+ throw new TodoStoreError(
281
+ 'STRUCTURE_ARTIFACT_INVALID', diagnostics[0].reason, undefined, { diagnostics },
282
+ );
283
+ }
284
+ }
274
285
  const todo = projectTodoStatus(store, {
275
286
  planNotes: await readTodoPlanNotesForStatus({ repoRoot, store }),
276
287
  parallelCandidates: await readTodoParallelCandidatesForStatus({ repoRoot, store, gitHead }),
@@ -314,6 +325,7 @@ async function resolveProjectState({ cwd, cliVersion }) {
314
325
  });
315
326
  return { exitCode: 0, repoRoot, store, todo, result };
316
327
  } catch (error) {
328
+ if (error?.code === 'STRUCTURE_ARTIFACT_INVALID') throw error;
317
329
  const reason = error instanceof TodoStoreError
318
330
  ? `${error.code}:${error.detail?.reason ?? error.message}` : 'store_validation_failed';
319
331
  return invalid(reason);
@@ -322,7 +334,9 @@ async function resolveProjectState({ cwd, cliVersion }) {
322
334
 
323
335
  export async function runProjectStatus({ cwd, stdout, cliVersion, env = process.env,
324
336
  ensureDashboardActivity = ensureTodoDashboardActivity }) {
325
- const state = await resolveProjectState({ cwd, cliVersion });
337
+ const state = await resolveProjectState({
338
+ cwd, cliVersion, diagnoseStructureArtifacts: true,
339
+ });
326
340
  // dashboard活動の登録はdiscovery面の副作用として維持する(ADR 0131 Decision 4で
327
341
  // session-context側だけが持たない、と決めた面である)。
328
342
  if (state.store !== null && env.LATTICE_DASHBOARD_AUTOSTART !== '0') {
@@ -680,6 +694,15 @@ export async function runPlanShow({ cwd, planKey, stdout }) {
680
694
  }
681
695
 
682
696
  export function projectStatusFailure({ cwd, stdout, cliVersion, error }) {
697
+ if (error?.code === 'STRUCTURE_ARTIFACT_INVALID') {
698
+ const payload = {
699
+ schema: 'lattice.cli_error.v2', code: error.code,
700
+ message: error.message ?? 'structure artifact invalid',
701
+ detail: error.detail,
702
+ };
703
+ stdout.write(`${JSON.stringify(payload)}\n`);
704
+ return 1;
705
+ }
683
706
  const projectRootConflict = error?.code === 'PROJECT_ROOT_CONFLICT';
684
707
  const result = invalidStatus({
685
708
  cliVersion, repoRoot: resolveRepoRoot(cwd),
@@ -642,7 +642,7 @@ export function validateExecutorPacket(value) {
642
642
  && identifier(packet.plan_ref)
643
643
  && nonNegativeInteger(packet.plan_epoch)
644
644
  && boundedArray(packet.verifier_refs, (entry) => typeof entry === 'string')
645
- && boundedArray(packet.forbidden_operations, (entry) => typeof entry === 'string', { min: 1 })
645
+ && boundedArray(packet.forbidden_operations, (entry) => typeof entry === 'string')
646
646
  && packet.context_content_digest === computeContextContentDigest(packet)
647
647
  && selfDigestValid(packet, 'packet_digest')
648
648
  ));