frontend-project-context 1.8.0 → 1.10.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/CHANGELOG.md +29 -1
- package/README.md +26 -7
- package/UPGRADING.md +24 -0
- package/docs/00-PRODUCT-CONSTITUTION.md +44 -12
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +9 -3
- package/docs/14-FORMAL-RELEASE-READINESS.md +14 -0
- package/docs/28-REAL-PROJECT-ONBOARDING-CLOSURE-DESIGN.md +10 -0
- package/docs/29-REAL-PROJECT-1.8.0-INITIALIZATION-OBSERVATIONS.md +228 -0
- package/docs/30-TASK-CONTEXT-CONSUMPTION-CLOSURE-DESIGN.md +563 -0
- package/docs/31-TASK-CONTEXT-INTEGRITY-REPAIR-DESIGN.md +281 -0
- package/docs/32-PROJECT-TO-TASK-INTERACTION-PROPOSAL.md +173 -0
- package/docs/33-BOUNDED-TASK-HANDOFF-REDESIGN.md +207 -0
- package/docs/34-A0-CODEX-HOST-FEASIBILITY.md +61 -0
- package/docs/35-CONTEXT-FIRST-TASK-HANDOFF-DESIGN.md +248 -0
- package/docs/36-PRODUCT-VALUE-AND-USABILITY-REVIEW.md +158 -0
- package/docs/37-EVERYDAY-HANDOFF-FLOW-DESIGN.md +271 -0
- package/docs/38-EVERYDAY-HANDOFF-OPERATION.md +147 -0
- package/docs/39-SELF-HOST-RELEASE-ACCEPTANCE.md +39 -0
- package/docs/AI-PROJECT-INITIALIZATION.md +8 -3
- package/docs/PRODUCT-SHARING-AND-ADOPTION-GUIDE.md +111 -0
- package/docs/README.md +26 -2
- package/docs/USER-AND-AI-OPERATION-MANUAL.md +9 -3
- package/docs/assets/product-sharing-01-overview.svg +32 -0
- package/docs/assets/product-sharing-02-how-it-works.svg +17 -0
- package/docs/assets/product-sharing-03-example.svg +19 -0
- package/examples/package.json +1 -1
- package/migration-manifest.json +38 -12
- package/package.json +2 -2
- package/schemas/capabilities.schema.json +26 -12
- package/schemas/context-bundle.schema.json +32 -0
- package/schemas/coverage-audit.schema.json +6 -4
- package/schemas/evidence-bundle.schema.json +1 -1
- package/schemas/initialization-instruction.schema.json +17 -5
- package/schemas/migration-manifest.schema.json +3 -3
- package/schemas/migration-plan.schema.json +1 -1
- package/schemas/project-brief.schema.json +400 -0
- package/schemas/projection-lock.schema.json +1 -1
- package/schemas/task-context-record.schema.json +27 -0
- package/schemas/task-handoff-input.schema.json +23 -0
- package/schemas/task-handoff-result.schema.json +320 -0
- package/schemas/upgrade-assessment.schema.json +1 -1
- package/schemas/upgrade-result-bundle.schema.json +1 -1
- package/schemas/work-view.schema.json +541 -0
- package/src/project-context/adaptive-context-schema.mjs +1 -1
- package/src/project-context/adaptive-context.mjs +16 -29
- package/src/project-context/ai-entry.mjs +28 -12
- package/src/project-context/approver.mjs +6 -2
- package/src/project-context/authoring.mjs +28 -8
- package/src/project-context/capabilities.mjs +13 -1
- package/src/project-context/checker.mjs +1 -1
- package/src/project-context/cli.mjs +74 -17
- package/src/project-context/context-bundle.mjs +379 -0
- package/src/project-context/contract-schema.mjs +62 -9
- package/src/project-context/coverage-profile.mjs +127 -0
- package/src/project-context/exchange-schema.mjs +24 -6
- package/src/project-context/exchange.mjs +3 -0
- package/src/project-context/initialization-instruction.mjs +20 -2
- package/src/project-context/maintenance.mjs +5 -4
- package/src/project-context/migration-manifest.mjs +3 -3
- package/src/project-context/path-policy.mjs +13 -0
- package/src/project-context/renderer.mjs +11 -4
- package/src/project-context/source-reader.mjs +27 -2
- package/src/project-context/task-context.mjs +4 -14
- package/src/project-context/task-handoff-schema.mjs +185 -0
- package/src/project-context/task-handoff.mjs +259 -0
- package/src/project-context/upgrade-schema.mjs +1 -1
- package/src/project-context/upgrade.mjs +1 -0
- package/src/project-context/work-view-schema.mjs +125 -0
- package/src/project-context/work-view.mjs +135 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { canonicalJson, digestJson } from "./canonical-json.mjs";
|
|
2
|
+
import { fail } from "./errors.mjs";
|
|
3
|
+
import { normalizeRelativePath } from "./path-policy.mjs";
|
|
4
|
+
import { scopeApplies } from "./scope-compiler.mjs";
|
|
5
|
+
|
|
6
|
+
const COVERAGE_SUBJECT = "project.registration-coverage";
|
|
7
|
+
const DISPOSITIONS = new Set(["contract-covered", "dynamic-investigation", "excluded-approved", "unresolved"]);
|
|
8
|
+
|
|
9
|
+
function uniqueSorted(values) {
|
|
10
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function object(value, label) {
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail("coverage-profile-invalid", `${label} must be an object`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function exactKeys(value, required, optional, label) {
|
|
18
|
+
object(value, label);
|
|
19
|
+
const allowed = new Set([...required, ...optional]);
|
|
20
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) fail("coverage-profile-invalid", `${label} contains unknown field: ${key}`);
|
|
21
|
+
for (const key of required) if (!Object.hasOwn(value, key)) fail("coverage-profile-invalid", `${label}.${key} is required`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function strings(values, label) {
|
|
25
|
+
if (!Array.isArray(values) || values.some((value) => typeof value !== "string")) fail("coverage-profile-invalid", `${label} must be a string array`);
|
|
26
|
+
return values;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizePath(value, label) {
|
|
30
|
+
if (typeof value !== "string") fail("coverage-profile-invalid", `${label} must be a string`);
|
|
31
|
+
try {
|
|
32
|
+
return normalizeRelativePath(value, { allowRoot: true, label });
|
|
33
|
+
} catch {
|
|
34
|
+
fail("coverage-profile-invalid", `${label} must be a valid project-relative path`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizedPaths(values, label) {
|
|
39
|
+
const normalized = strings(values, label).map((value) => normalizePath(value, `${label} entry`));
|
|
40
|
+
return uniqueSorted(normalized);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function coverageProfile(contract) {
|
|
44
|
+
const candidates = contract.items.filter((item) => item.status === "approved" && item.kind === "policy" && item.subject === COVERAGE_SUBJECT);
|
|
45
|
+
if (candidates.length === 0) return null;
|
|
46
|
+
if (candidates.length > 1) fail("coverage-profile-conflict", `multiple approved ${COVERAGE_SUBJECT} items are active`);
|
|
47
|
+
const item = candidates[0];
|
|
48
|
+
const value = item.value;
|
|
49
|
+
object(value, `${item.id} coverage value`);
|
|
50
|
+
if (value.version === 2) {
|
|
51
|
+
exactKeys(value, ["version", "domains"], [], `${item.id} coverage v2 value`);
|
|
52
|
+
if (!Array.isArray(value.domains)) fail("coverage-profile-invalid", `${item.id} coverage v2 domains must be an array`);
|
|
53
|
+
const approved = new Map(contract.items.filter((entry) => entry.status === "approved").map((entry) => [entry.id, entry]));
|
|
54
|
+
const ids = new Set();
|
|
55
|
+
const paths = new Set();
|
|
56
|
+
const domains = value.domains.map((domain, index) => {
|
|
57
|
+
const label = `${item.id} domains[${index}]`;
|
|
58
|
+
exactKeys(domain, ["id", "path", "disposition", "itemIds"], ["rationale"], label);
|
|
59
|
+
if (typeof domain.id !== "string" || domain.id.length === 0 || ids.has(domain.id)) fail("coverage-profile-invalid", `${label}.id must be unique`);
|
|
60
|
+
ids.add(domain.id);
|
|
61
|
+
const normalizedPath = normalizePath(domain.path, `${label}.path`);
|
|
62
|
+
if (normalizedPath !== domain.path || paths.has(normalizedPath)) fail("coverage-profile-invalid", `${label}.path must be normalized and unique`);
|
|
63
|
+
paths.add(normalizedPath);
|
|
64
|
+
if (!DISPOSITIONS.has(domain.disposition)) fail("coverage-profile-invalid", `${label}.disposition is invalid`);
|
|
65
|
+
const itemIds = uniqueSorted(strings(domain.itemIds, `${label}.itemIds`));
|
|
66
|
+
if (canonicalJson(itemIds) !== canonicalJson(domain.itemIds)) fail("coverage-profile-invalid", `${label}.itemIds must be sorted and unique`);
|
|
67
|
+
if (domain.disposition === "contract-covered") {
|
|
68
|
+
if (itemIds.length === 0) fail("coverage-profile-invalid", `${label} contract-covered domain requires itemIds`);
|
|
69
|
+
for (const itemId of itemIds) {
|
|
70
|
+
const covered = approved.get(itemId);
|
|
71
|
+
if (!covered || !scopeApplies(covered.scope, normalizedPath)) fail("coverage-profile-invalid", `${label} references an item that is not effective for its path: ${itemId}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (domain.disposition === "excluded-approved" && (typeof domain.rationale !== "string" || domain.rationale.length === 0)) {
|
|
75
|
+
fail("coverage-profile-invalid", `${label} excluded-approved domain requires rationale`);
|
|
76
|
+
}
|
|
77
|
+
if (domain.rationale !== undefined && typeof domain.rationale !== "string") fail("coverage-profile-invalid", `${label}.rationale must be a string`);
|
|
78
|
+
return {
|
|
79
|
+
id: domain.id,
|
|
80
|
+
path: normalizedPath,
|
|
81
|
+
disposition: domain.disposition,
|
|
82
|
+
itemIds,
|
|
83
|
+
...(domain.rationale !== undefined ? { rationale: domain.rationale } : {}),
|
|
84
|
+
};
|
|
85
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
86
|
+
if (canonicalJson(domains) !== canonicalJson(value.domains)) fail("coverage-profile-invalid", `${item.id} domains must be stably sorted by id`);
|
|
87
|
+
return { itemId: item.id, itemDigest: digestJson(item), sourceIds: [...item.sources].sort(), version: 2, domains };
|
|
88
|
+
}
|
|
89
|
+
exactKeys(value, ["roots", "excludedPaths"], [], `${item.id} coverage value`);
|
|
90
|
+
return {
|
|
91
|
+
itemId: item.id,
|
|
92
|
+
itemDigest: digestJson(item),
|
|
93
|
+
sourceIds: [...item.sources].sort(),
|
|
94
|
+
version: 1,
|
|
95
|
+
roots: normalizedPaths(value.roots, "coverage roots"),
|
|
96
|
+
excludedPaths: normalizedPaths(value.excludedPaths, "coverage excluded paths"),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function pathWithin(target, parent) {
|
|
101
|
+
return parent === "." || target === parent || target.startsWith(`${parent}/`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function coverageDomainForPath(profile, candidatePath) {
|
|
105
|
+
if (!profile || profile.version !== 2) return null;
|
|
106
|
+
return profile.domains
|
|
107
|
+
.filter((entry) => pathWithin(candidatePath, entry.path))
|
|
108
|
+
.sort((left, right) => right.path.length - left.path.length || left.id.localeCompare(right.id))[0] ?? null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function coverageClass(profile, registeredPaths, candidatePath) {
|
|
112
|
+
if (registeredPaths.has(candidatePath)) return "registered";
|
|
113
|
+
if (!profile) return "outside-declared-coverage";
|
|
114
|
+
if (profile.version === 2) {
|
|
115
|
+
const domain = coverageDomainForPath(profile, candidatePath);
|
|
116
|
+
if (!domain) return "outside-declared-coverage";
|
|
117
|
+
return {
|
|
118
|
+
"contract-covered": "registered",
|
|
119
|
+
"dynamic-investigation": "dynamic-investigation",
|
|
120
|
+
"excluded-approved": "excluded-approved",
|
|
121
|
+
unresolved: "unresolved",
|
|
122
|
+
}[domain.disposition];
|
|
123
|
+
}
|
|
124
|
+
if (profile.excludedPaths.some((entry) => pathWithin(candidatePath, entry))) return "excluded-approved";
|
|
125
|
+
if (profile.roots.some((entry) => pathWithin(candidatePath, entry))) return "review-required";
|
|
126
|
+
return "outside-declared-coverage";
|
|
127
|
+
}
|
|
@@ -2,11 +2,11 @@ import { canonicalValue, digestJson, validateJsonValue } from "./canonical-json.
|
|
|
2
2
|
import { fail } from "./errors.mjs";
|
|
3
3
|
import { normalizeRelativePath } from "./path-policy.mjs";
|
|
4
4
|
|
|
5
|
-
export const PACKAGE_VERSION = "1.
|
|
6
|
-
export const EXCHANGE_PROTOCOL_VERSION =
|
|
5
|
+
export const PACKAGE_VERSION = "1.10.0";
|
|
6
|
+
export const EXCHANGE_PROTOCOL_VERSION = 9;
|
|
7
7
|
export const ACTION_PLAN_SCHEMA_VERSION = 2;
|
|
8
8
|
export const REVIEW_BUNDLE_SCHEMA_VERSION = 2;
|
|
9
|
-
export const CAPABILITIES_SCHEMA_VERSION =
|
|
9
|
+
export const CAPABILITIES_SCHEMA_VERSION = 11;
|
|
10
10
|
|
|
11
11
|
export const ACTION_KINDS = Object.freeze([
|
|
12
12
|
"accept-source-change",
|
|
@@ -27,6 +27,7 @@ export const COMMANDS = Object.freeze([
|
|
|
27
27
|
"index-context", "instructions", "publish-entry", "remove-entry", "review-source", "revise", "setup", "stage-context", "status", "sync",
|
|
28
28
|
"upgrade-apply", "upgrade-check", "upgrade-plan",
|
|
29
29
|
"reconcile-truth",
|
|
30
|
+
"project-brief", "task-handoff",
|
|
30
31
|
]);
|
|
31
32
|
|
|
32
33
|
const ACTION_KIND_SET = new Set(ACTION_KINDS);
|
|
@@ -143,7 +144,7 @@ function normalizeScope(value, label) {
|
|
|
143
144
|
function normalizeItemInput(input, label, extra = []) {
|
|
144
145
|
object(input, label);
|
|
145
146
|
exactKeys(input, new Set([
|
|
146
|
-
"id", "kind", "subject", "value", "statement", "sources", "scope", "overrides", "verification", ...extra,
|
|
147
|
+
"id", "kind", "subject", "value", "statement", "sources", "scope", "overrides", "verification", "consumption", ...extra,
|
|
147
148
|
]), label);
|
|
148
149
|
stableId(input.id, `${label}.id`);
|
|
149
150
|
if (!ITEM_KINDS.has(input.kind)) invalid(`${label}.kind is invalid`);
|
|
@@ -159,6 +160,16 @@ function normalizeItemInput(input, label, extra = []) {
|
|
|
159
160
|
sources.forEach((entry) => stableId(entry, `${label}.sources entry`));
|
|
160
161
|
const overrides = uniqueStrings(input.overrides ?? [], `${label}.overrides`, { empty: true });
|
|
161
162
|
overrides.forEach((entry) => stableId(entry, `${label}.overrides entry`));
|
|
163
|
+
let consumption;
|
|
164
|
+
if (input.consumption !== undefined) {
|
|
165
|
+
object(input.consumption, `${label}.consumption`);
|
|
166
|
+
exactKeys(input.consumption, new Set(["requiredSources", "conditionalSources"]), `${label}.consumption`);
|
|
167
|
+
const requiredSources = uniqueStrings(input.consumption.requiredSources, `${label}.consumption.requiredSources`, { empty: true });
|
|
168
|
+
const conditionalSources = uniqueStrings(input.consumption.conditionalSources, `${label}.consumption.conditionalSources`, { empty: true });
|
|
169
|
+
for (const source of [...requiredSources, ...conditionalSources]) if (!sources.includes(source)) invalid(`${label}.consumption source must be included in sources: ${source}`);
|
|
170
|
+
if (requiredSources.some((source) => conditionalSources.includes(source))) invalid(`${label}.consumption roles must be mutually exclusive`);
|
|
171
|
+
consumption = { requiredSources, conditionalSources };
|
|
172
|
+
}
|
|
162
173
|
return {
|
|
163
174
|
id: input.id,
|
|
164
175
|
kind: input.kind,
|
|
@@ -168,18 +179,23 @@ function normalizeItemInput(input, label, extra = []) {
|
|
|
168
179
|
sources,
|
|
169
180
|
scope: normalizeScope(input.scope, `${label}.scope`),
|
|
170
181
|
overrides,
|
|
182
|
+
...(consumption ? { consumption } : {}),
|
|
171
183
|
...(input.verification !== undefined ? { verification: normalizeVerification(input.verification, `${label}.verification`) } : {}),
|
|
172
184
|
};
|
|
173
185
|
}
|
|
174
186
|
|
|
175
187
|
function normalizeSourceInput(input, label) {
|
|
176
188
|
object(input, label);
|
|
177
|
-
exactKeys(input, new Set(["id", "kind", "path", "pointer", "reference"]), label);
|
|
189
|
+
exactKeys(input, new Set(["id", "kind", "path", "pointer", "reference", "digestMode"]), label);
|
|
178
190
|
stableId(input.id, `${label}.id`);
|
|
179
191
|
if (!SOURCE_KINDS.has(input.kind)) invalid(`${label}.kind is invalid`);
|
|
180
192
|
if (["file", "path", "json-pointer"].includes(input.kind)) {
|
|
181
193
|
if (input.reference !== undefined) invalid(`${label}.reference is not allowed for local sources`);
|
|
182
194
|
const normalized = { id: input.id, kind: input.kind, path: projectPath(input.path, `${label}.path`) };
|
|
195
|
+
if (input.digestMode !== undefined) {
|
|
196
|
+
if (input.kind !== "file" || !["full-file", "outside-owned-ai-entry"].includes(input.digestMode)) invalid(`${label}.digestMode is invalid`);
|
|
197
|
+
normalized.digestMode = input.digestMode;
|
|
198
|
+
}
|
|
183
199
|
if (input.kind === "json-pointer") {
|
|
184
200
|
string(input.pointer, `${label}.pointer`, { empty: true });
|
|
185
201
|
if (input.pointer !== "" && !input.pointer.startsWith("/")) invalid(`${label}.pointer must use RFC 6901 syntax`);
|
|
@@ -187,7 +203,7 @@ function normalizeSourceInput(input, label) {
|
|
|
187
203
|
} else if (input.pointer !== undefined) invalid(`${label}.pointer is only allowed for json-pointer`);
|
|
188
204
|
return normalized;
|
|
189
205
|
}
|
|
190
|
-
if (input.path !== undefined || input.pointer !== undefined) invalid(`${label} cannot contain a local path`);
|
|
206
|
+
if (input.path !== undefined || input.pointer !== undefined || input.digestMode !== undefined) invalid(`${label} cannot contain a local path or digest mode`);
|
|
191
207
|
string(input.reference, `${label}.reference`);
|
|
192
208
|
return { id: input.id, kind: input.kind, reference: input.reference };
|
|
193
209
|
}
|
|
@@ -531,6 +547,8 @@ export function itemPrimitiveInput(input) {
|
|
|
531
547
|
scope: input.scope.kind,
|
|
532
548
|
scopePath: input.scope.path,
|
|
533
549
|
overrides: [...input.overrides],
|
|
550
|
+
requiredSources: [...(input.consumption?.requiredSources ?? [])],
|
|
551
|
+
conditionalSources: [...(input.consumption?.conditionalSources ?? [])],
|
|
534
552
|
verification: input.verification?.kind,
|
|
535
553
|
verificationSource: input.verification?.source,
|
|
536
554
|
verificationExpectedPresent: Boolean(input.verification && Object.hasOwn(input.verification, "expected")),
|
|
@@ -71,6 +71,8 @@ function itemArgs(input, options = {}) {
|
|
|
71
71
|
"--scope", input.scope.kind,
|
|
72
72
|
...(input.scope.path ? ["--scope-path", input.scope.path] : []),
|
|
73
73
|
...(input.overrides.length > 0 ? ["--overrides", ...input.overrides] : []),
|
|
74
|
+
...(input.consumption?.requiredSources?.length > 0 ? ["--required-source", ...input.consumption.requiredSources] : []),
|
|
75
|
+
...(input.consumption?.conditionalSources?.length > 0 ? ["--conditional-source", ...input.consumption.conditionalSources] : []),
|
|
74
76
|
];
|
|
75
77
|
if (input.verification) {
|
|
76
78
|
args.push("--verification", input.verification.kind);
|
|
@@ -89,6 +91,7 @@ function sourceArgs(input) {
|
|
|
89
91
|
...(input.path ? ["--path", input.path] : []),
|
|
90
92
|
...(input.pointer !== undefined ? ["--pointer", input.pointer] : []),
|
|
91
93
|
...(input.reference ? ["--reference", input.reference] : []),
|
|
94
|
+
...(input.digestMode ? ["--digest-mode", input.digestMode] : []),
|
|
92
95
|
];
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { sha256 } from "./canonical-json.mjs";
|
|
4
5
|
import { PACKAGE_VERSION } from "./exchange-schema.mjs";
|
|
5
6
|
|
|
6
|
-
export const INITIALIZATION_INSTRUCTION_SCHEMA_VERSION =
|
|
7
|
+
export const INITIALIZATION_INSTRUCTION_SCHEMA_VERSION = 2;
|
|
7
8
|
export const INITIALIZATION_INSTRUCTION_ID = "ai-project-initialization";
|
|
8
|
-
export const INITIALIZATION_INSTRUCTION_VERSION =
|
|
9
|
+
export const INITIALIZATION_INSTRUCTION_VERSION = 2;
|
|
9
10
|
export const INITIALIZATION_INSTRUCTION_PACKAGE_PATH = "docs/AI-PROJECT-INITIALIZATION.md";
|
|
10
11
|
export const INITIALIZATION_INSTRUCTION_COMMAND = "project-context instructions --project PATH [--json | --prompt]";
|
|
11
12
|
|
|
@@ -23,12 +24,29 @@ export async function readInitializationInstruction() {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export async function buildInitializationInstruction(targetRoot, boundaries) {
|
|
27
|
+
let entryContent = null;
|
|
28
|
+
try {
|
|
29
|
+
entryContent = await readFile(path.join(targetRoot, "AGENTS.md"), "utf8");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error?.code !== "ENOENT") throw error;
|
|
32
|
+
}
|
|
26
33
|
return {
|
|
27
34
|
schemaVersion: INITIALIZATION_INSTRUCTION_SCHEMA_VERSION,
|
|
28
35
|
package: { name: "frontend-project-context", version: PACKAGE_VERSION },
|
|
29
36
|
instruction: await readInitializationInstruction(),
|
|
30
37
|
targetRoot,
|
|
31
38
|
boundaries: { ...boundaries },
|
|
39
|
+
entryRuleReview: {
|
|
40
|
+
path: "AGENTS.md",
|
|
41
|
+
present: entryContent !== null,
|
|
42
|
+
allowedDispositions: ["migrated-to-contract", "kept-in-entry-intentionally", "excluded-as-stale", "unresolved-blocker"],
|
|
43
|
+
records: entryContent === null || entryContent.trim().length === 0 ? [] : [{
|
|
44
|
+
id: "human-entry-content",
|
|
45
|
+
disposition: "unresolved-blocker",
|
|
46
|
+
rationale: "Classify every duplicated or legacy human entry rule in the concentrated initialization review before completion.",
|
|
47
|
+
}],
|
|
48
|
+
automaticDeletion: false,
|
|
49
|
+
},
|
|
32
50
|
};
|
|
33
51
|
}
|
|
34
52
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { digestJson } from "./canonical-json.mjs";
|
|
2
2
|
import { buildItemProposal } from "./authoring.mjs";
|
|
3
|
-
import { sourceStatus, validateContract, validateSourceLock } from "./contract-schema.mjs";
|
|
3
|
+
import { promoteContractToSchema3, sourceStatus, validateContract, validateSourceLock } from "./contract-schema.mjs";
|
|
4
4
|
import { fail } from "./errors.mjs";
|
|
5
5
|
import { writeProjectState } from "./project-store.mjs";
|
|
6
6
|
import { readSourceDigest } from "./source-reader.mjs";
|
|
@@ -104,7 +104,7 @@ export async function reviewSource(root, project, sourceId, options = {}) {
|
|
|
104
104
|
let status;
|
|
105
105
|
let reason;
|
|
106
106
|
try {
|
|
107
|
-
currentDigest = await readSourceDigest(root, source, options.sourceReadContext);
|
|
107
|
+
currentDigest = await readSourceDigest(root, source, options.sourceReadContext, { projectionsLock: project.projectionsLock });
|
|
108
108
|
if (lockedDigest === null || lockedDigest !== source.digest) {
|
|
109
109
|
status = "unreadable";
|
|
110
110
|
reason = lockedDigest === null ? "source-lock-missing" : "source-lock-mismatch";
|
|
@@ -179,7 +179,7 @@ export async function acceptSourceChange(root, project, input, options = {}) {
|
|
|
179
179
|
if (options.write) {
|
|
180
180
|
let actual;
|
|
181
181
|
try {
|
|
182
|
-
actual = await readSourceDigest(root, source);
|
|
182
|
+
actual = await readSourceDigest(root, source, undefined, { projectionsLock: project.projectionsLock });
|
|
183
183
|
} catch (error) {
|
|
184
184
|
fail("source-accept-digest-mismatch", "source became unavailable while acceptance was being prepared", {
|
|
185
185
|
exitCode: 1,
|
|
@@ -266,7 +266,7 @@ export async function deprecateSource(project, input, options = {}) {
|
|
|
266
266
|
|
|
267
267
|
const timestamp = input.at ?? new Date().toISOString();
|
|
268
268
|
const nextContract = structuredClone(project.contract);
|
|
269
|
-
nextContract.schemaVersion = 2;
|
|
269
|
+
if (nextContract.schemaVersion < 2) nextContract.schemaVersion = 2;
|
|
270
270
|
nextContract.sources = nextContract.sources.map((source) => {
|
|
271
271
|
const active = source.status === undefined ? { ...source, status: "active" } : source;
|
|
272
272
|
if (source.id !== input.id) return active;
|
|
@@ -326,6 +326,7 @@ export async function reviseItem(root, project, input, options = {}) {
|
|
|
326
326
|
});
|
|
327
327
|
}
|
|
328
328
|
const nextContract = structuredClone(project.contract);
|
|
329
|
+
if (replacement.consumption !== undefined) promoteContractToSchema3(nextContract);
|
|
329
330
|
nextContract.items[nextContract.items.findIndex((item) => item.id === input.id)] = replacement;
|
|
330
331
|
validateContract(nextContract);
|
|
331
332
|
if (options.write) await writeProjectState(project, nextContract, project.sourcesLock, options.storeOptions);
|
|
@@ -22,7 +22,7 @@ const MIGRATION_KINDS = new Set([
|
|
|
22
22
|
]);
|
|
23
23
|
const ROLLBACK_CLASSES = new Set(["package-only", "reversible-data", "forward-only"]);
|
|
24
24
|
const CONSUMER_CHANGE_KEYS = new Set([
|
|
25
|
-
"actionPlan", "adaptiveContextBundle", "capabilities", "contextQuery", "coverageAudit", "evidenceBundle", "evidenceInput", "exchange",
|
|
25
|
+
"actionPlan", "adaptiveContextBundle", "capabilities", "contextBundle", "contextQuery", "coverageAudit", "evidenceBundle", "evidenceInput", "exchange",
|
|
26
26
|
"hostPromotionEvidence", "integrationReviewBundle", "reviewBundle", "routingIndex", "stageContextBundle", "stageReceipt", "taskContextPlan",
|
|
27
27
|
"truthReconciliationInput", "truthReconciliationReviewBundle", "initializationInstruction", "projectStatus",
|
|
28
28
|
]);
|
|
@@ -103,7 +103,7 @@ export function validateMigrationManifest(input) {
|
|
|
103
103
|
]), "migration manifest");
|
|
104
104
|
if (manifest.schemaVersion !== MIGRATION_MANIFEST_SCHEMA_VERSION) invalid("migration manifest schemaVersion must be 2");
|
|
105
105
|
exactKeys(manifest.package, new Set(["name", "version"]), "migration manifest package");
|
|
106
|
-
if (manifest.package.name !== "frontend-project-context" || manifest.package.version !== "1.
|
|
106
|
+
if (manifest.package.name !== "frontend-project-context" || manifest.package.version !== "1.10.0") invalid("migration manifest package does not match this runtime");
|
|
107
107
|
versions(manifest.upgradeFrom, "upgradeFrom");
|
|
108
108
|
if (manifest.upgradeFrom.length === 0) invalid("upgradeFrom must not be empty");
|
|
109
109
|
exactKeys(manifest.stores, new Set(["contract", "projectionLock", "proposal", "sourceLock"]), "stores");
|
|
@@ -111,7 +111,7 @@ export function validateMigrationManifest(input) {
|
|
|
111
111
|
exactKeys(manifest.renderers, new Set(["aiEntry", "projection"]), "renderers");
|
|
112
112
|
for (const name of Object.keys(manifest.renderers)) versionMatrix(manifest.renderers[name], `renderers.${name}`);
|
|
113
113
|
exactKeys(manifest.protocols, new Set([
|
|
114
|
-
"exchange", "actionPlan", "adaptiveContextBundle", "contextQuery", "coverageAudit", "reviewBundle", "evidenceInput", "evidenceBundle", "taskContextPlan",
|
|
114
|
+
"exchange", "actionPlan", "adaptiveContextBundle", "contextBundle", "contextQuery", "coverageAudit", "reviewBundle", "evidenceInput", "evidenceBundle", "taskContextPlan",
|
|
115
115
|
"routingIndex", "stageReceipt", "stageContextBundle", "integrationReviewBundle", "hostPromotionEvidence",
|
|
116
116
|
"truthReconciliationInput", "truthReconciliationReviewBundle", "initializationInstruction", "projectStatus",
|
|
117
117
|
]), "protocols");
|
|
@@ -134,6 +134,19 @@ export async function resolveWritableInside(root, relativePath, options = {}) {
|
|
|
134
134
|
return { normalized, absolute };
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// Context can name a file that has not been created yet. Validate the nearest
|
|
138
|
+
// existing parent as well as existing targets, without creating any directory.
|
|
139
|
+
export async function inspectContextTarget(root, relativePath) {
|
|
140
|
+
try {
|
|
141
|
+
await resolveExistingInside(root, relativePath, { allowRoot: true, label: "context target" });
|
|
142
|
+
return "existing";
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error?.code !== "source-missing") throw error;
|
|
145
|
+
await resolveWritableInside(root, relativePath, { allowRoot: true, label: "context target" });
|
|
146
|
+
return "prospective-or-deleted";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
137
150
|
export function assertProjectionPath(target, normalized) {
|
|
138
151
|
if (target === "agents") {
|
|
139
152
|
if (path.posix.basename(normalized) !== "AGENTS.md") {
|
|
@@ -64,7 +64,7 @@ function groupedSections(sections) {
|
|
|
64
64
|
return groups;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function
|
|
67
|
+
export function renderCollectedContextBundle(bundle, sources, task, options = {}) {
|
|
68
68
|
const locale = options.locale ?? "zh-CN";
|
|
69
69
|
const lines = [
|
|
70
70
|
"# Project Context Bundle",
|
|
@@ -100,11 +100,18 @@ function renderCollectedBundle(bundle, sources, task, options = {}) {
|
|
|
100
100
|
lines.push(`- \`${id}\` — ${source?.kind ?? "unknown"}: \`${location}\``);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
if ((options.gaps ?? []).length > 0) {
|
|
104
|
+
lines.push("", "## Context gaps", "");
|
|
105
|
+
for (const gap of options.gaps) lines.push(`- \`${gap.code}\`${gap.path ? ` — \`${gap.path}\`` : ""}`);
|
|
106
|
+
}
|
|
107
|
+
if ((options.nextActions ?? []).length > 0) {
|
|
108
|
+
lines.push("", "## Next actions", "", ...options.nextActions.map((action) => `- \`${action}\``));
|
|
109
|
+
}
|
|
103
110
|
return `${lines.join("\n")}\n`;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
export function renderContextBundle(contract, paths, task, options = {}) {
|
|
107
|
-
return
|
|
114
|
+
return renderCollectedContextBundle(collectBundle(contract, paths), contract.sources, task, options);
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
export function renderSelectedContextBundle(contract, paths, itemIds, task, options = {}) {
|
|
@@ -122,7 +129,7 @@ export function renderSelectedContextBundle(contract, paths, itemIds, task, opti
|
|
|
122
129
|
sections,
|
|
123
130
|
itemIds: [...selected].sort(),
|
|
124
131
|
};
|
|
125
|
-
return
|
|
132
|
+
return renderCollectedContextBundle(bundle, contract.sources, task, options);
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
// Task delivery deliberately has its own renderer. Managed AGENTS/Ruler projections
|
|
@@ -183,7 +190,7 @@ export function renderTaskContextBundle(contract, paths, itemIds, task, options
|
|
|
183
190
|
|
|
184
191
|
export function renderProjection(contract, paths, target) {
|
|
185
192
|
const bundle = collectBundle(contract, paths);
|
|
186
|
-
const body =
|
|
193
|
+
const body = renderCollectedContextBundle(bundle, contract.sources);
|
|
187
194
|
const bundleDigest = sha256(body);
|
|
188
195
|
const marker = `<!-- managed-by: project-context; renderer-version: ${RENDERER_VERSION}; target: ${target}; contract-digest: ${bundle.contractDigest}; bundle-digest: ${bundleDigest} -->`;
|
|
189
196
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { canonicalJson, sha256 } from "./canonical-json.mjs";
|
|
3
|
+
import { canonicalJson, digestJson, sha256 } from "./canonical-json.mjs";
|
|
4
|
+
import { AI_ENTRY_REGION_ID, parseAiEntryRegion } from "./ai-entry.mjs";
|
|
4
5
|
import { sourceStatus } from "./contract-schema.mjs";
|
|
5
6
|
import { fail } from "./errors.mjs";
|
|
6
7
|
import { resolveExistingInside } from "./path-policy.mjs";
|
|
@@ -97,10 +98,34 @@ export async function digestPathIdentity(projectRoot, relativePath, context) {
|
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
async function digestOutsideOwnedAiEntry(projectRoot, source, context, projectionsLock) {
|
|
102
|
+
const lock = projectionsLock?.projections?.find((entry) => entry.path === source.path);
|
|
103
|
+
if (!lock || lock.ownership !== "region" || lock.target !== "ai-entry" || lock.regionId !== AI_ENTRY_REGION_ID) {
|
|
104
|
+
fail("source-digest-ownership-invalid", `outside-owned-ai-entry requires a trusted AI Entry lock: ${source.path}`);
|
|
105
|
+
}
|
|
106
|
+
const { absolute } = await resolveExistingInside(projectRoot, source.path);
|
|
107
|
+
const content = await cached(context, `body:${absolute}`, async () => {
|
|
108
|
+
if (context?.metrics) context.metrics.sourceBodyReads += 1;
|
|
109
|
+
return readFile(absolute, "utf8");
|
|
110
|
+
});
|
|
111
|
+
const parsed = parseAiEntryRegion(content);
|
|
112
|
+
if (parsed.state !== "present") fail("source-digest-marker-invalid", `AI Entry markers are not valid: ${source.path}`);
|
|
113
|
+
if (sha256(parsed.region) !== lock.regionDigest) fail("source-digest-ownership-invalid", `AI Entry region digest does not match its lock: ${source.path}`);
|
|
114
|
+
const renderer = /project-context:ai-entry; schema-version: 1; renderer-version: (\d+)/u.exec(parsed.region)?.[1];
|
|
115
|
+
if (renderer === undefined || Number(renderer) !== lock.rendererVersion) {
|
|
116
|
+
fail("source-digest-ownership-invalid", `AI Entry renderer does not match its lock: ${source.path}`);
|
|
117
|
+
}
|
|
118
|
+
return digestJson({ before: content.slice(0, parsed.start), after: content.slice(parsed.end) });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function readSourceDigest(projectRoot, source, context, options = {}) {
|
|
101
122
|
if (sourceStatus(source) === "deprecated") return null;
|
|
102
123
|
if (source.kind === "human-decision" || source.kind === "external-reference") return null;
|
|
103
124
|
if (source.kind === "path") return digestPathIdentity(projectRoot, source.path, context);
|
|
125
|
+
if (source.kind === "file" && source.digestMode === "outside-owned-ai-entry") {
|
|
126
|
+
return cached(context, `digest:outside-owned-ai-entry:${source.path}`, () =>
|
|
127
|
+
digestOutsideOwnedAiEntry(projectRoot, source, context, options.projectionsLock));
|
|
128
|
+
}
|
|
104
129
|
if (source.kind === "file") return digestPath(projectRoot, source.path, context);
|
|
105
130
|
return sha256(canonicalJson(await readJsonSourceValue(projectRoot, source, context)));
|
|
106
131
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { canonicalJson, digestJson } from "./canonical-json.mjs";
|
|
2
|
-
import { selectAdaptiveItems } from "./adaptive-context.mjs";
|
|
3
2
|
import { blockingContextFindings, checkProject } from "./checker.mjs";
|
|
4
3
|
import { ProjectContextError, fail } from "./errors.mjs";
|
|
5
4
|
import { readJsonFile } from "./io.mjs";
|
|
@@ -92,18 +91,10 @@ function normalizedReceipts(receiptInputs, plan) {
|
|
|
92
91
|
return byStage;
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
function selectedItems(contract, targetPaths, findings
|
|
94
|
+
function selectedItems(contract, targetPaths, findings) {
|
|
96
95
|
const byId = new Map();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const selection = selectAdaptiveItems(contract, null, {
|
|
100
|
-
task: { text: adaptiveText, topics: [], paths: targetPaths, changedPaths: [], itemIds: [] },
|
|
101
|
-
level: "initial",
|
|
102
|
-
}, { findings: adaptiveFindings });
|
|
103
|
-
findings.push(...adaptiveFindings);
|
|
104
|
-
for (const item of selection.selected) byId.set(item.id, item);
|
|
105
|
-
}
|
|
106
|
-
if (adaptiveText === undefined) {
|
|
96
|
+
// Schema-1 plans have no adaptive delivery/expansion contract. Preserve their
|
|
97
|
+
// complete scoped context; the existing budget gate blocks oversized bundles.
|
|
107
98
|
for (const targetPath of targetPaths) {
|
|
108
99
|
try {
|
|
109
100
|
for (const item of effectiveItems(contract.items, targetPath)) byId.set(item.id, item);
|
|
@@ -113,7 +104,6 @@ function selectedItems(contract, targetPaths, findings, adaptiveText) {
|
|
|
113
104
|
for (const item of contract.items.filter((entry) => entry.status === "approved" && scopeApplies(entry.scope, targetPath))) byId.set(item.id, item);
|
|
114
105
|
}
|
|
115
106
|
}
|
|
116
|
-
}
|
|
117
107
|
return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id)).map((item) => ({
|
|
118
108
|
id: item.id,
|
|
119
109
|
kind: item.kind,
|
|
@@ -217,7 +207,7 @@ async function buildStageContextBundleCore(root, project, plan, options, receipt
|
|
|
217
207
|
if (receipt.status !== "completed") findings.push(finding("stage-dependency-blocked", "blocked", { stageId: stage.id, dependencyStageId: dependencyId }));
|
|
218
208
|
}
|
|
219
209
|
const targetPaths = uniqueSorted([...stage.paths, ...changedPaths]);
|
|
220
|
-
const contractItems = selectedItems(project.contract, targetPaths, findings
|
|
210
|
+
const contractItems = selectedItems(project.contract, targetPaths, findings);
|
|
221
211
|
const readTargets = mergeReadTargets([
|
|
222
212
|
...stage.paths.map((entry) => ({ path: entry, reason: "stage-scope" })),
|
|
223
213
|
...changedPaths.map((entry) => ({ path: entry, reason: "host-changed-path-signal" })),
|