@deftai/directive-core 0.65.0 → 0.66.1

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.
Files changed (152) hide show
  1. package/dist/architecture/sor-preflight.js +6 -4
  2. package/dist/capacity/backfill.js +5 -3
  3. package/dist/capacity/show.js +5 -3
  4. package/dist/category-b-namespace/index.d.ts +39 -0
  5. package/dist/category-b-namespace/index.js +120 -0
  6. package/dist/codebase/default-extractor.js +2 -1
  7. package/dist/codebase/map.js +2 -1
  8. package/dist/codebase/provider.js +4 -2
  9. package/dist/content-contracts/standards/_helpers.js +1 -1
  10. package/dist/doctor/checks.js +2 -1
  11. package/dist/doctor/doctor-state.js +2 -2
  12. package/dist/encoding/patterns.js +19 -0
  13. package/dist/encoding/scan.js +3 -2
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +2 -0
  16. package/dist/init-deposit/refresh.js +2 -1
  17. package/dist/init-deposit/scaffold.js +2 -1
  18. package/dist/install-upgrade/index.d.ts +2 -0
  19. package/dist/install-upgrade/index.js +31 -2
  20. package/dist/intake/issue-emit.d.ts +1 -1
  21. package/dist/intake/issue-emit.js +3 -2
  22. package/dist/intake/issue-ingest.d.ts +24 -0
  23. package/dist/intake/issue-ingest.js +113 -16
  24. package/dist/intake/reconcile-issues.js +11 -4
  25. package/dist/layout/index.d.ts +2 -0
  26. package/dist/layout/index.js +2 -0
  27. package/dist/layout/resolve.d.ts +87 -0
  28. package/dist/layout/resolve.js +159 -0
  29. package/dist/lifecycle/lifecycle-hygiene.d.ts +1 -1
  30. package/dist/lifecycle/lifecycle-hygiene.js +8 -5
  31. package/dist/orchestration/judgment-policy.js +4 -6
  32. package/dist/orchestration/verify-judgment-gates.d.ts +1 -0
  33. package/dist/orchestration/verify-judgment-gates.js +4 -2
  34. package/dist/policy/autonomy.js +4 -6
  35. package/dist/policy/capacity.js +4 -6
  36. package/dist/policy/decisions.d.ts +1 -0
  37. package/dist/policy/decisions.js +4 -2
  38. package/dist/policy/index.d.ts +1 -0
  39. package/dist/policy/index.js +3 -1
  40. package/dist/policy/plan-extensions.d.ts +35 -0
  41. package/dist/policy/plan-extensions.js +57 -0
  42. package/dist/policy/resolve.d.ts +6 -2
  43. package/dist/policy/resolve.js +18 -8
  44. package/dist/policy/wip.js +6 -4
  45. package/dist/preflight-cache/evaluate.js +8 -5
  46. package/dist/release/native-steps.js +4 -3
  47. package/dist/render/framework-commands.js +2 -1
  48. package/dist/render/prd-render.js +2 -2
  49. package/dist/render/project-render.js +23 -5
  50. package/dist/render/roadmap-render.js +36 -8
  51. package/dist/render/scope-outlook.js +3 -2
  52. package/dist/scope/audit-log.js +3 -2
  53. package/dist/scope/capacity-stamp.js +2 -1
  54. package/dist/scope/decompose.js +6 -4
  55. package/dist/scope/decomposed-refs.js +2 -1
  56. package/dist/scope/demote.js +5 -4
  57. package/dist/scope/project-context.js +2 -1
  58. package/dist/scope/project-definition-sync.js +4 -3
  59. package/dist/scope/transition.js +6 -2
  60. package/dist/scope/undo.js +2 -1
  61. package/dist/scope/vbrief-ref.js +5 -3
  62. package/dist/session/resume-conditions.js +2 -1
  63. package/dist/session/ritual-sentinel.js +2 -1
  64. package/dist/session/staleness.js +2 -1
  65. package/dist/slice/record.js +4 -4
  66. package/dist/spec-authority/resolver.js +5 -5
  67. package/dist/swarm/complete-cohort.js +5 -4
  68. package/dist/swarm/launch.js +6 -7
  69. package/dist/swarm/readiness.js +22 -20
  70. package/dist/swarm/subagent-backend.js +2 -5
  71. package/dist/swarm/verify-review-clean.js +2 -1
  72. package/dist/triage/actions/candidates-log.d.ts +3 -0
  73. package/dist/triage/actions/candidates-log.js +8 -2
  74. package/dist/triage/bootstrap/gitignore.js +22 -13
  75. package/dist/triage/bootstrap/index.js +7 -4
  76. package/dist/triage/bulk/index.js +2 -1
  77. package/dist/triage/classify/index.js +13 -11
  78. package/dist/triage/queue/audit.js +3 -3
  79. package/dist/triage/queue/cache.js +3 -2
  80. package/dist/triage/queue/ranking-labels.js +2 -1
  81. package/dist/triage/queue/scope-walk.js +8 -6
  82. package/dist/triage/reconcile/audit.js +2 -1
  83. package/dist/triage/reconcile/parse-uri.js +2 -1
  84. package/dist/triage/reconcile/reconcile.js +8 -7
  85. package/dist/triage/refresh/extract.js +4 -2
  86. package/dist/triage/refresh/refresh.js +3 -2
  87. package/dist/triage/scope/mutations-core.js +13 -9
  88. package/dist/triage/scope/renderers.js +5 -3
  89. package/dist/triage/scope/resolve.js +6 -5
  90. package/dist/triage/scope/validate.js +3 -6
  91. package/dist/triage/scope-drift/add-ignore.js +4 -2
  92. package/dist/triage/scope-drift/scope-rules.js +3 -2
  93. package/dist/triage/subscribe/index.js +13 -7
  94. package/dist/triage/summary/index.js +7 -5
  95. package/dist/triage/summary/reconcilable.js +6 -4
  96. package/dist/triage/summary/scope-drift.js +2 -1
  97. package/dist/triage/welcome/prior-state.js +9 -9
  98. package/dist/triage/welcome/summary.js +9 -8
  99. package/dist/triage/welcome/writers.js +12 -10
  100. package/dist/validate-content/capacity-policy.js +5 -4
  101. package/dist/validate-content/capacity-show.js +5 -3
  102. package/dist/validate-content/filename.d.ts +1 -1
  103. package/dist/validate-content/filename.js +5 -4
  104. package/dist/validate-content/validate-strategy-output.js +14 -5
  105. package/dist/vbrief-build/build.js +1 -1
  106. package/dist/vbrief-build/constants.js +7 -0
  107. package/dist/vbrief-build/speckit.js +3 -2
  108. package/dist/vbrief-reconcile/graph.js +3 -2
  109. package/dist/vbrief-reconcile/labels.js +4 -3
  110. package/dist/vbrief-reconcile/reconciliation.js +5 -1
  111. package/dist/vbrief-reconcile/swarm-deps.js +6 -5
  112. package/dist/vbrief-reconcile/umbrellas.d.ts +1 -1
  113. package/dist/vbrief-reconcile/umbrellas.js +9 -7
  114. package/dist/vbrief-validate/conformance.d.ts +1 -2
  115. package/dist/vbrief-validate/conformance.js +29 -14
  116. package/dist/vbrief-validate/constants.d.ts +18 -1
  117. package/dist/vbrief-validate/constants.js +32 -3
  118. package/dist/vbrief-validate/epic-links.js +3 -2
  119. package/dist/vbrief-validate/filename.js +5 -3
  120. package/dist/vbrief-validate/index.d.ts +3 -1
  121. package/dist/vbrief-validate/index.js +2 -1
  122. package/dist/vbrief-validate/main.js +17 -6
  123. package/dist/vbrief-validate/origin.js +1 -1
  124. package/dist/vbrief-validate/paths.js +2 -1
  125. package/dist/vbrief-validate/plan-hooks.js +4 -3
  126. package/dist/vbrief-validate/registry-status.d.ts +1 -0
  127. package/dist/vbrief-validate/registry-status.js +3 -1
  128. package/dist/vbrief-validate/roundtrip.d.ts +36 -0
  129. package/dist/vbrief-validate/roundtrip.js +168 -0
  130. package/dist/vbrief-validate/schema.d.ts +1 -1
  131. package/dist/vbrief-validate/schema.js +62 -19
  132. package/dist/vbrief-validate/validate-all.js +12 -4
  133. package/dist/verify-source/code-structure-validate.js +8 -4
  134. package/dist/verify-source/contract-drift.d.ts +7 -3
  135. package/dist/verify-source/contract-drift.js +40 -22
  136. package/dist/xbrief-migrate/constants.d.ts +13 -0
  137. package/dist/xbrief-migrate/constants.js +14 -0
  138. package/dist/xbrief-migrate/detect.d.ts +11 -0
  139. package/dist/xbrief-migrate/detect.js +81 -0
  140. package/dist/xbrief-migrate/drift-gate.d.ts +56 -0
  141. package/dist/xbrief-migrate/drift-gate.js +205 -0
  142. package/dist/xbrief-migrate/index.d.ts +7 -0
  143. package/dist/xbrief-migrate/index.js +7 -0
  144. package/dist/xbrief-migrate/migrate-project.d.ts +33 -0
  145. package/dist/xbrief-migrate/migrate-project.js +189 -0
  146. package/dist/xbrief-migrate/signpost.d.ts +5 -0
  147. package/dist/xbrief-migrate/signpost.js +19 -0
  148. package/dist/xbrief-migrate/transforms.d.ts +46 -0
  149. package/dist/xbrief-migrate/transforms.js +182 -0
  150. package/dist/xbrief-migrate/version.d.ts +8 -0
  151. package/dist/xbrief-migrate/version.js +25 -0
  152. package/package.json +11 -3
@@ -0,0 +1,168 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { isExtensionKey } from "@deftai/directive-types";
4
+ import { validateVbriefSchema } from "./schema.js";
5
+ /** Relative path to extension round-trip conformance fixtures (#715). */
6
+ export const EXTENSION_CONFORMANCE_FIXTURES_DIR = "content/vbrief/conformance/extensions/valid";
7
+ /** Thrown when `reEmitVbriefArtifact` rejects an artifact that fails schema validation. */
8
+ export class VbriefSchemaValidationError extends Error {
9
+ relPath;
10
+ errors;
11
+ constructor(relPath, errors) {
12
+ super(`schema validation failed for ${relPath}: ${errors.join("; ")}`);
13
+ this.relPath = relPath;
14
+ this.errors = errors;
15
+ this.name = "VbriefSchemaValidationError";
16
+ }
17
+ }
18
+ /** Recursively collect every `x-<consumer>/` key in `value` using the contract pattern. */
19
+ export function collectExtensionEntries(value, jsonPath = "$") {
20
+ if (Array.isArray(value)) {
21
+ const entries = [];
22
+ for (let index = 0; index < value.length; index += 1) {
23
+ entries.push(...collectExtensionEntries(value[index], `${jsonPath}[${index}]`));
24
+ }
25
+ return entries;
26
+ }
27
+ if (typeof value !== "object" || value === null) {
28
+ return [];
29
+ }
30
+ const obj = value;
31
+ const entries = [];
32
+ for (const [key, entryValue] of Object.entries(obj)) {
33
+ const childPath = `${jsonPath}.${JSON.stringify(key)}`;
34
+ if (isExtensionKey(key)) {
35
+ entries.push({ jsonPath: childPath, key, value: entryValue });
36
+ }
37
+ entries.push(...collectExtensionEntries(entryValue, childPath));
38
+ }
39
+ return entries;
40
+ }
41
+ function entriesByPath(entries) {
42
+ const map = new Map();
43
+ for (const entry of entries) {
44
+ map.set(entry.jsonPath, entry.value);
45
+ }
46
+ return map;
47
+ }
48
+ /** Compare extension keys/values between two parsed artifacts (JSON structural equality). */
49
+ export function findExtensionPreservationViolations(original, roundtripped) {
50
+ const before = collectExtensionEntries(original);
51
+ const afterMap = entriesByPath(collectExtensionEntries(roundtripped));
52
+ const violations = [];
53
+ for (const entry of before) {
54
+ if (!afterMap.has(entry.jsonPath)) {
55
+ violations.push(`dropped extension key '${entry.key}' at ${entry.jsonPath}`);
56
+ continue;
57
+ }
58
+ const afterValue = afterMap.get(entry.jsonPath);
59
+ if (JSON.stringify(entry.value) !== JSON.stringify(afterValue)) {
60
+ violations.push(`mutated extension key '${entry.key}' at ${entry.jsonPath}`);
61
+ }
62
+ }
63
+ return violations;
64
+ }
65
+ /**
66
+ * Canonical reference-consumer read/write pipeline: validate, serialize, re-parse.
67
+ * Extension properties MUST survive this path verbatim (xBRIEF v0.8 §7).
68
+ */
69
+ export function reEmitVbriefArtifact(artifact, relPath = "<roundtrip>") {
70
+ const schemaErrors = validateVbriefSchema(artifact, relPath);
71
+ if (schemaErrors.length > 0) {
72
+ throw new VbriefSchemaValidationError(relPath, schemaErrors);
73
+ }
74
+ const serialized = `${JSON.stringify(artifact, null, 2)}\n`;
75
+ return JSON.parse(serialized);
76
+ }
77
+ export function renderExtensionRoundtripFinding(finding) {
78
+ return ` ${finding.path}: ${finding.message}`;
79
+ }
80
+ /** Run extension round-trip preservation over packaged conformance fixtures. */
81
+ export function evaluateExtensionRoundtrip(projectRoot) {
82
+ const root = resolve(projectRoot);
83
+ const fixturesDir = join(root, EXTENSION_CONFORMANCE_FIXTURES_DIR);
84
+ if (!existsSync(fixturesDir)) {
85
+ return {
86
+ exitCode: 0,
87
+ findings: [],
88
+ message: "",
89
+ };
90
+ }
91
+ let fixtureNames;
92
+ try {
93
+ fixtureNames = readdirSync(fixturesDir, { withFileTypes: true })
94
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".vbrief.json"))
95
+ .map((entry) => entry.name)
96
+ .sort();
97
+ }
98
+ catch (err) {
99
+ const e = err;
100
+ return {
101
+ exitCode: 2,
102
+ findings: [],
103
+ message: `\u274c verify_vbrief_conformance: cannot read extension fixtures: ${e.message ?? err}`,
104
+ };
105
+ }
106
+ if (fixtureNames.length === 0) {
107
+ return {
108
+ exitCode: 2,
109
+ findings: [],
110
+ message: `\u274c verify_vbrief_conformance: no *.vbrief.json fixtures under ` +
111
+ `${EXTENSION_CONFORMANCE_FIXTURES_DIR}/.`,
112
+ };
113
+ }
114
+ const findings = [];
115
+ for (const name of fixtureNames) {
116
+ const relPath = `${EXTENSION_CONFORMANCE_FIXTURES_DIR}/${name}`;
117
+ const fullPath = join(fixturesDir, name);
118
+ let text;
119
+ try {
120
+ text = readFileSync(fullPath, "utf8");
121
+ }
122
+ catch (err) {
123
+ const e = err;
124
+ findings.push({
125
+ path: relPath,
126
+ message: `unreadable fixture: ${e.message ?? String(err)}`,
127
+ });
128
+ continue;
129
+ }
130
+ let parsed;
131
+ try {
132
+ parsed = JSON.parse(text);
133
+ }
134
+ catch (err) {
135
+ findings.push({
136
+ path: relPath,
137
+ message: `invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
138
+ });
139
+ continue;
140
+ }
141
+ const schemaErrors = validateVbriefSchema(parsed, relPath);
142
+ for (const error of schemaErrors) {
143
+ findings.push({ path: relPath, message: error });
144
+ }
145
+ if (schemaErrors.length > 0) {
146
+ continue;
147
+ }
148
+ const roundtripped = reEmitVbriefArtifact(parsed, relPath);
149
+ for (const violation of findExtensionPreservationViolations(parsed, roundtripped)) {
150
+ findings.push({ path: relPath, message: violation });
151
+ }
152
+ }
153
+ if (findings.length > 0) {
154
+ const header = `\u274c verify_vbrief_conformance: extension round-trip preservation failed ` +
155
+ `(${findings.length} finding(s), #715).\n` +
156
+ " Every key matching ^x-[a-z0-9-]+/ MUST survive load->re-emit verbatim at every object level.";
157
+ const body = findings.slice(0, 50).map(renderExtensionRoundtripFinding).join("\n");
158
+ const tail = findings.length > 50 ? `\n ... and ${findings.length - 50} more` : "";
159
+ return { exitCode: 1, findings, message: `${header}\n${body}${tail}` };
160
+ }
161
+ return {
162
+ exitCode: 0,
163
+ findings,
164
+ message: `\u2713 verify_vbrief_conformance: ${fixtureNames.length} extension fixture(s) ` +
165
+ "round-trip clean (#715).",
166
+ };
167
+ }
168
+ //# sourceMappingURL=roundtrip.js.map
@@ -1,5 +1,5 @@
1
1
  export type JsonObject = Record<string, unknown>;
2
- /** Validate vBRIEF structural requirements (v0.6). */
2
+ /** Validate vBRIEF/xBRIEF structural requirements (v0.6 + v0.8 additive). */
3
3
  export declare function validateVbriefSchema(data: JsonObject, filepath: string): string[];
4
4
  /** Normalize a narrative key for D3 comparison. */
5
5
  export declare function normalizeNarrativeKey(key: string): string;
@@ -1,5 +1,5 @@
1
1
  import { pyStrRepr, pythonTypeName } from "../triage/scope/python-repr.js";
2
- import { PROJECT_DEF_EXPECTED_NARRATIVES, VALID_STATUSES, VALID_VBRIEF_VERSIONS, } from "./constants.js";
2
+ import { PROJECT_DEF_EXPECTED_NARRATIVES, VALID_INFO_ROOT_KEYS, VALID_ITEM_STATUSES, VALID_PLAN_ITEM_TYPES, VALID_PLAN_STATUSES, VALID_VBRIEF_VERSIONS, } from "./constants.js";
3
3
  function validateNarratives(narratives, path, errors) {
4
4
  if (typeof narratives !== "object" || narratives === null || Array.isArray(narratives)) {
5
5
  errors.push(`${path} must be an object`);
@@ -11,6 +11,30 @@ function validateNarratives(narratives, path, errors) {
11
11
  }
12
12
  }
13
13
  }
14
+ function validatePlanRefs(planRefs, path, errors) {
15
+ if (!Array.isArray(planRefs)) {
16
+ errors.push(`${path}.planRefs must be an array`);
17
+ return;
18
+ }
19
+ for (let i = 0; i < planRefs.length; i += 1) {
20
+ if (typeof planRefs[i] !== "string") {
21
+ errors.push(`${path}.planRefs[${i}] must be a string, got ${pythonTypeName(planRefs[i])}`);
22
+ }
23
+ }
24
+ }
25
+ function resolveInfoBlock(data) {
26
+ for (const key of VALID_INFO_ROOT_KEYS) {
27
+ if (!(key in data)) {
28
+ continue;
29
+ }
30
+ const info = data[key];
31
+ if (typeof info !== "object" || info === null || Array.isArray(info)) {
32
+ return null;
33
+ }
34
+ return { key: key, info: info };
35
+ }
36
+ return null;
37
+ }
14
38
  function validatePlanItem(item, path, errors) {
15
39
  const itemId = typeof item.id === "string" ? item.id : "<no-id>";
16
40
  const itemPath = `${path}[${itemId}]`;
@@ -20,9 +44,18 @@ function validatePlanItem(item, path, errors) {
20
44
  if (!("status" in item)) {
21
45
  errors.push(`${itemPath} missing 'status'`);
22
46
  }
23
- else if (!VALID_STATUSES.has(String(item.status))) {
47
+ else if (!VALID_ITEM_STATUSES.has(String(item.status))) {
24
48
  errors.push(`${itemPath} invalid status: ${pyStrRepr(String(item.status))}`);
25
49
  }
50
+ if ("type" in item && !VALID_PLAN_ITEM_TYPES.has(String(item.type))) {
51
+ errors.push(`${itemPath} invalid type: ${pyStrRepr(String(item.type))}`);
52
+ }
53
+ if ("summary" in item && typeof item.summary !== "string") {
54
+ errors.push(`${itemPath}.summary must be a string, got ${pythonTypeName(item.summary)}`);
55
+ }
56
+ if ("planRefs" in item) {
57
+ validatePlanRefs(item.planRefs, itemPath, errors);
58
+ }
26
59
  if ("narrative" in item) {
27
60
  validateNarratives(item.narrative, `${itemPath}.narrative`, errors);
28
61
  }
@@ -57,25 +90,35 @@ function validatePlanItem(item, path, errors) {
57
90
  }
58
91
  }
59
92
  }
60
- /** Validate vBRIEF structural requirements (v0.6). */
93
+ /** Validate vBRIEF/xBRIEF structural requirements (v0.6 + v0.8 additive). */
61
94
  export function validateVbriefSchema(data, filepath) {
62
95
  const errors = [];
63
- if (!("vBRIEFInfo" in data)) {
64
- errors.push(`${filepath}: missing required top-level key 'vBRIEFInfo'`);
96
+ const resolved = resolveInfoBlock(data);
97
+ if (resolved === null) {
98
+ let infoShapeError = false;
99
+ for (const key of VALID_INFO_ROOT_KEYS) {
100
+ if (!(key in data)) {
101
+ continue;
102
+ }
103
+ const info = data[key];
104
+ if (info === null || Array.isArray(info) || typeof info !== "object") {
105
+ errors.push(`${filepath}: '${key}' must be an object`);
106
+ infoShapeError = true;
107
+ break;
108
+ }
109
+ }
110
+ if (!infoShapeError) {
111
+ errors.push(`${filepath}: missing required top-level key 'vBRIEFInfo' or 'xBRIEFInfo'`);
112
+ }
65
113
  }
66
114
  else {
67
- const info = data.vBRIEFInfo;
68
- if (typeof info !== "object" || info === null || Array.isArray(info)) {
69
- errors.push(`${filepath}: 'vBRIEFInfo' must be an object`);
70
- }
71
- else {
72
- const version = info.version;
73
- if (!VALID_VBRIEF_VERSIONS.has(String(version))) {
74
- errors.push(`${filepath}: 'vBRIEFInfo.version' must be '0.6' ` +
75
- `(canonical v0.6 schema, #533), got ` +
76
- `${pyStrRepr(String(version))}. Run \`task migrate:vbrief\` to ` +
77
- `upgrade pre-existing v0.5 vBRIEFs in-place.`);
78
- }
115
+ const version = resolved.info.version;
116
+ if (!VALID_VBRIEF_VERSIONS.has(String(version))) {
117
+ errors.push(`${filepath}: '${resolved.key}.version' must be one of ` +
118
+ `${[...VALID_VBRIEF_VERSIONS].map((v) => `'${v}'`).join(", ")} ` +
119
+ `(canonical v0.6/v0.8 schema, #2107), got ` +
120
+ `${pyStrRepr(String(version))}. Run \`task migrate:vbrief\` to ` +
121
+ `upgrade pre-existing v0.5 vBRIEFs in-place.`);
79
122
  }
80
123
  }
81
124
  if (!("plan" in data)) {
@@ -96,8 +139,8 @@ export function validateVbriefSchema(data, filepath) {
96
139
  if ("title" in planObj && (typeof planObj.title !== "string" || !planObj.title)) {
97
140
  errors.push(`${filepath}: 'plan.title' must be a non-empty string`);
98
141
  }
99
- if ("status" in planObj && !VALID_STATUSES.has(String(planObj.status))) {
100
- const sorted = [...VALID_STATUSES]
142
+ if ("status" in planObj && !VALID_PLAN_STATUSES.has(String(planObj.status))) {
143
+ const sorted = [...VALID_PLAN_STATUSES]
101
144
  .sort()
102
145
  .map((s) => `'${s}'`)
103
146
  .join(", ");
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { hasArtifactSuffix, MIGRATED_ARTIFACT_DIR } from "../layout/resolve.js";
3
4
  import { LIFECYCLE_FOLDERS } from "./constants.js";
4
5
  import { validateNoRootDecompositionDrafts } from "./decomposition.js";
5
6
  import { validateEpicStoryLinks } from "./epic-links.js";
@@ -36,7 +37,7 @@ export function discoverVbriefs(vbriefDir) {
36
37
  continue;
37
38
  }
38
39
  const names = readdirSync(folderPath)
39
- .filter((name) => name.endsWith(".vbrief.json"))
40
+ .filter((name) => hasArtifactSuffix(name))
40
41
  .sort();
41
42
  for (const name of names) {
42
43
  files.push({
@@ -89,8 +90,15 @@ export function validateAll(vbriefDir, options = {}) {
89
90
  warnings.push(...validateOriginProvenance(display, data, vbriefDir, strictOriginTypes));
90
91
  }
91
92
  const normalizedDir = normalizeVbriefDir(vbriefDir);
92
- const projectDefDisplay = `${normalizedDir}/PROJECT-DEFINITION.vbrief.json`;
93
- const projectDefAbsolute = join(vbriefDir, "PROJECT-DEFINITION.vbrief.json");
93
+ // Layout-aware (#2109 part 1): key the project-definition suffix to the
94
+ // resolved lifecycle directory's identity (its basename) rather than probing
95
+ // the filesystem, so a stray xbrief-named file in a legacy tree cannot flip
96
+ // selection -- this stays consistent with resolveLifecycleLayout's output.
97
+ const projectDefName = basename(normalizedDir) === MIGRATED_ARTIFACT_DIR
98
+ ? "PROJECT-DEFINITION.xbrief.json"
99
+ : "PROJECT-DEFINITION.vbrief.json";
100
+ const projectDefDisplay = `${normalizedDir}/${projectDefName}`;
101
+ const projectDefAbsolute = join(vbriefDir, projectDefName);
94
102
  if (existsSync(projectDefAbsolute)) {
95
103
  const { data, error } = loadVbrief(projectDefAbsolute);
96
104
  if (error !== null) {
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { hasArtifactSuffix, projectDefinitionRelPath, resolveLifecycleRoot, resolveProjectDefinitionPath, } from "../layout/resolve.js";
3
4
  export const CODE_STRUCTURE_VERSION = "0.1";
4
5
  export const DIRECTIVE_HOME = "x-directive/architecture.codeStructure";
5
6
  export const PLAN_HOME = "plan.architecture.codeStructure";
@@ -495,7 +496,9 @@ export function validateFile(path, options = {}) {
495
496
  }
496
497
  if (projectRoot !== null && !allowStandalone) {
497
498
  const relPath = projectRelative(path, projectRoot);
498
- if (relPath !== PROJECT_DEFINITION_PATH && homes.length > 0) {
499
+ // Layout-aware (#2109 part 2a): compare against the resolved lifecycle's
500
+ // PROJECT-DEFINITION rel path (vbrief/... on today's tree).
501
+ if (relPath !== projectDefinitionRelPath(projectRoot) && homes.length > 0) {
499
502
  errors.push(finding("CS-HOME", "canonical codeStructure metadata must live in vbrief/PROJECT-DEFINITION.vbrief.json; sibling files must be generated projections", path));
500
503
  }
501
504
  }
@@ -529,7 +532,8 @@ export function validateFile(path, options = {}) {
529
532
  }
530
533
  export function discoverCodeStructurePaths(projectRoot) {
531
534
  const paths = new Map();
532
- const projectDef = join(projectRoot, "vbrief", "PROJECT-DEFINITION.vbrief.json");
535
+ // Layout-aware (#2109 part 2a): resolve PROJECT-DEFINITION + lifecycle root.
536
+ const projectDef = resolveProjectDefinitionPath(projectRoot);
533
537
  if (existsSync(projectDef)) {
534
538
  try {
535
539
  const data = loadJsonFile(projectDef);
@@ -541,7 +545,7 @@ export function discoverCodeStructurePaths(projectRoot) {
541
545
  paths.set(projectDef.replace(/\\/g, "/"), projectDef);
542
546
  }
543
547
  }
544
- const vbriefRoot = join(projectRoot, "vbrief");
548
+ const vbriefRoot = resolveLifecycleRoot(projectRoot);
545
549
  if (existsSync(vbriefRoot)) {
546
550
  const stack = [vbriefRoot];
547
551
  while (stack.length > 0) {
@@ -562,7 +566,7 @@ export function discoverCodeStructurePaths(projectRoot) {
562
566
  stack.push(full);
563
567
  continue;
564
568
  }
565
- if (!name.endsWith(".vbrief.json") || full === projectDef) {
569
+ if (!hasArtifactSuffix(name) || full === projectDef) {
566
570
  continue;
567
571
  }
568
572
  try {
@@ -1,11 +1,13 @@
1
1
  /**
2
- * contract-drift.ts — deterministic gate for the public contract layer (#1799).
2
+ * contract-drift.ts — deterministic gate for the public contract layer (#1799, #2107).
3
3
  *
4
4
  * Ensures:
5
5
  * 1. packages/types/schemas/vbrief-core-0.6.schema.json matches the canonical
6
6
  * content/vbrief/schemas/vbrief-core.schema.json byte-for-byte.
7
- * 2. @deftai/directive-types Status enum matches the schema Status enum.
8
- * 3. @deftai/directive-types VBRIEF_VERSION matches the schema version const.
7
+ * 2. packages/types/schemas/xbrief-core-0.8.schema.json matches the canonical
8
+ * content/vbrief/schemas/xbrief-core-0.8.schema.json byte-for-byte.
9
+ * 3. @deftai/directive-types VALID_STATUSES matches v0.6 $defs.Status enum.
10
+ * 4. @deftai/directive-types VBRIEF_VERSION matches v0.8 $defs.xBRIEFInfo.version const.
9
11
  *
10
12
  * Exit codes: 0 clean / 1 drift / 2 config error.
11
13
  */
@@ -15,6 +17,8 @@ export declare const EXIT_DRIFT = 1;
15
17
  export declare const EXIT_CONFIG_ERROR = 2;
16
18
  export declare const CANONICAL_SCHEMA_REL = "content/vbrief/schemas/vbrief-core.schema.json";
17
19
  export declare const PUBLISHED_SCHEMA_REL = "packages/types/schemas/vbrief-core-0.6.schema.json";
20
+ export declare const XBRIEF_CANONICAL_SCHEMA_REL = "content/vbrief/schemas/xbrief-core-0.8.schema.json";
21
+ export declare const XBRIEF_PUBLISHED_SCHEMA_REL = "packages/types/schemas/xbrief-core-0.8.schema.json";
18
22
  export interface ContractDriftResult {
19
23
  readonly code: GateExitCode;
20
24
  readonly message: string;
@@ -1,11 +1,13 @@
1
1
  /**
2
- * contract-drift.ts — deterministic gate for the public contract layer (#1799).
2
+ * contract-drift.ts — deterministic gate for the public contract layer (#1799, #2107).
3
3
  *
4
4
  * Ensures:
5
5
  * 1. packages/types/schemas/vbrief-core-0.6.schema.json matches the canonical
6
6
  * content/vbrief/schemas/vbrief-core.schema.json byte-for-byte.
7
- * 2. @deftai/directive-types Status enum matches the schema Status enum.
8
- * 3. @deftai/directive-types VBRIEF_VERSION matches the schema version const.
7
+ * 2. packages/types/schemas/xbrief-core-0.8.schema.json matches the canonical
8
+ * content/vbrief/schemas/xbrief-core-0.8.schema.json byte-for-byte.
9
+ * 3. @deftai/directive-types VALID_STATUSES matches v0.6 $defs.Status enum.
10
+ * 4. @deftai/directive-types VBRIEF_VERSION matches v0.8 $defs.xBRIEFInfo.version const.
9
11
  *
10
12
  * Exit codes: 0 clean / 1 drift / 2 config error.
11
13
  */
@@ -17,6 +19,8 @@ export const EXIT_DRIFT = 1;
17
19
  export const EXIT_CONFIG_ERROR = 2;
18
20
  export const CANONICAL_SCHEMA_REL = "content/vbrief/schemas/vbrief-core.schema.json";
19
21
  export const PUBLISHED_SCHEMA_REL = "packages/types/schemas/vbrief-core-0.6.schema.json";
22
+ export const XBRIEF_CANONICAL_SCHEMA_REL = "content/vbrief/schemas/xbrief-core-0.8.schema.json";
23
+ export const XBRIEF_PUBLISHED_SCHEMA_REL = "packages/types/schemas/xbrief-core-0.8.schema.json";
20
24
  function defaultReadText(path) {
21
25
  return readFileSync(path, "utf8");
22
26
  }
@@ -48,38 +52,35 @@ function schemaStatusEnum(schema) {
48
52
  }
49
53
  return [...enumValues];
50
54
  }
51
- function schemaVersionConst(schema) {
55
+ function xbriefVersionConst(schema) {
52
56
  const defs = schema.$defs;
53
57
  if (typeof defs !== "object" || defs === null || Array.isArray(defs)) {
54
58
  throw new Error("schema missing $defs");
55
59
  }
56
- const info = defs.vBRIEFInfo;
60
+ const info = defs.xBRIEFInfo;
57
61
  if (typeof info !== "object" || info === null || Array.isArray(info)) {
58
- throw new Error("schema missing $defs.vBRIEFInfo");
62
+ throw new Error("schema missing $defs.xBRIEFInfo");
59
63
  }
60
64
  const properties = info.properties;
61
65
  if (typeof properties !== "object" || properties === null || Array.isArray(properties)) {
62
- throw new Error("schema missing $defs.vBRIEFInfo.properties");
66
+ throw new Error("schema missing $defs.xBRIEFInfo.properties");
63
67
  }
64
68
  const version = properties.version;
65
69
  if (typeof version !== "object" || version === null || Array.isArray(version)) {
66
- throw new Error("schema missing $defs.vBRIEFInfo.properties.version");
70
+ throw new Error("schema missing $defs.xBRIEFInfo.properties.version");
67
71
  }
68
72
  const constValue = version.const;
69
73
  if (typeof constValue !== "string") {
70
- throw new Error("schema $defs.vBRIEFInfo.properties.version.const must be a string");
74
+ throw new Error("schema $defs.xBRIEFInfo.properties.version.const must be a string");
71
75
  }
72
76
  return constValue;
73
77
  }
74
78
  function sortedStrings(values) {
75
79
  return [...values].sort();
76
80
  }
77
- /** Evaluate contract drift for the directive source tree. */
78
- export function evaluateContractDrift(projectRoot, options = {}) {
79
- const root = resolve(projectRoot);
80
- const readText = options.readText ?? defaultReadText;
81
- const canonicalPath = join(root, CANONICAL_SCHEMA_REL);
82
- const publishedPath = join(root, PUBLISHED_SCHEMA_REL);
81
+ function schemaPairInSync(root, readText, canonicalRel, publishedRel) {
82
+ const canonicalPath = join(root, canonicalRel);
83
+ const publishedPath = join(root, publishedRel);
83
84
  let canonicalText;
84
85
  let publishedText;
85
86
  try {
@@ -97,14 +98,31 @@ export function evaluateContractDrift(projectRoot, options = {}) {
97
98
  if (canonicalText !== publishedText) {
98
99
  return {
99
100
  code: EXIT_DRIFT,
100
- message: `contract-drift: ${PUBLISHED_SCHEMA_REL} is out of sync with ${CANONICAL_SCHEMA_REL}. ` +
101
+ message: `contract-drift: ${publishedRel} is out of sync with ${canonicalRel}. ` +
101
102
  "Run: pnpm --prefix packages/types run prebuild",
102
103
  stream: "stderr",
103
104
  };
104
105
  }
105
- let schema;
106
+ return null;
107
+ }
108
+ /** Evaluate contract drift for the directive source tree. */
109
+ export function evaluateContractDrift(projectRoot, options = {}) {
110
+ const root = resolve(projectRoot);
111
+ const readText = options.readText ?? defaultReadText;
112
+ for (const [canonicalRel, publishedRel] of [
113
+ [CANONICAL_SCHEMA_REL, PUBLISHED_SCHEMA_REL],
114
+ [XBRIEF_CANONICAL_SCHEMA_REL, XBRIEF_PUBLISHED_SCHEMA_REL],
115
+ ]) {
116
+ const syncResult = schemaPairInSync(root, readText, canonicalRel, publishedRel);
117
+ if (syncResult !== null) {
118
+ return syncResult;
119
+ }
120
+ }
121
+ let v06Schema;
122
+ let v08Schema;
106
123
  try {
107
- schema = loadSchemaJson(canonicalText, CANONICAL_SCHEMA_REL);
124
+ v06Schema = loadSchemaJson(readText(join(root, CANONICAL_SCHEMA_REL)), CANONICAL_SCHEMA_REL);
125
+ v08Schema = loadSchemaJson(readText(join(root, XBRIEF_CANONICAL_SCHEMA_REL)), XBRIEF_CANONICAL_SCHEMA_REL);
108
126
  }
109
127
  catch (error) {
110
128
  const detail = error instanceof Error ? error.message : String(error);
@@ -115,7 +133,7 @@ export function evaluateContractDrift(projectRoot, options = {}) {
115
133
  };
116
134
  }
117
135
  try {
118
- const schemaStatuses = sortedStrings(schemaStatusEnum(schema));
136
+ const schemaStatuses = sortedStrings(schemaStatusEnum(v06Schema));
119
137
  const typeStatuses = sortedStrings(VALID_STATUSES);
120
138
  if (schemaStatuses.join("|") !== typeStatuses.join("|")) {
121
139
  return {
@@ -125,12 +143,12 @@ export function evaluateContractDrift(projectRoot, options = {}) {
125
143
  stream: "stderr",
126
144
  };
127
145
  }
128
- const schemaVersion = schemaVersionConst(schema);
146
+ const schemaVersion = xbriefVersionConst(v08Schema);
129
147
  if (schemaVersion !== VBRIEF_VERSION) {
130
148
  return {
131
149
  code: EXIT_DRIFT,
132
150
  message: "contract-drift: VBRIEF_VERSION in @deftai/directive-types diverges from " +
133
- "schema vBRIEFInfo.version const.",
151
+ "schema xBRIEFInfo.version const.",
134
152
  stream: "stderr",
135
153
  };
136
154
  }
@@ -145,7 +163,7 @@ export function evaluateContractDrift(projectRoot, options = {}) {
145
163
  }
146
164
  return {
147
165
  code: EXIT_OK,
148
- message: "contract-drift: canonical schema, published copy, and TS contract constants are in sync.",
166
+ message: "contract-drift: canonical schemas, published copies, and TS contract constants are in sync.",
149
167
  stream: "stdout",
150
168
  };
151
169
  }
@@ -0,0 +1,13 @@
1
+ import { LEGACY_VBRIEF_VERSION, VBRIEF_REFERENCE_PREFIX, VBRIEF_VERSION, XBRIEF_REFERENCE_PREFIX } from "@deftai/directive-types";
2
+ export { LEGACY_VBRIEF_VERSION, VBRIEF_REFERENCE_PREFIX, VBRIEF_VERSION, XBRIEF_REFERENCE_PREFIX };
3
+ /** PlanItem container `type` values introduced in xBRIEF v0.8 (excludes `task`). */
4
+ export declare const V08_CONTAINER_ITEM_TYPES: Set<string>;
5
+ export declare const LEGACY_ARTIFACT_DIR: "vbrief";
6
+ export declare const MIGRATED_ARTIFACT_DIR: "xbrief";
7
+ export declare const LEGACY_ARTIFACT_SUFFIX: ".vbrief.json";
8
+ export declare const MIGRATED_ARTIFACT_SUFFIX: ".xbrief.json";
9
+ export declare const LEGACY_INFO_ROOT_KEY: "vBRIEFInfo";
10
+ export declare const MIGRATED_INFO_ROOT_KEY: "xBRIEFInfo";
11
+ /** Item-only status value added in xBRIEF v0.8. */
12
+ export declare const V08_ITEM_STATUS_AUTO: "auto";
13
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { LEGACY_VBRIEF_VERSION, VBRIEF_REFERENCE_PREFIX, VBRIEF_VERSION, XBRIEF_REFERENCE_PREFIX, } from "@deftai/directive-types";
2
+ import { VALID_PLAN_ITEM_TYPES } from "../vbrief-validate/constants.js";
3
+ export { LEGACY_VBRIEF_VERSION, VBRIEF_REFERENCE_PREFIX, VBRIEF_VERSION, XBRIEF_REFERENCE_PREFIX };
4
+ /** PlanItem container `type` values introduced in xBRIEF v0.8 (excludes `task`). */
5
+ export const V08_CONTAINER_ITEM_TYPES = new Set([...VALID_PLAN_ITEM_TYPES].filter((t) => t !== "task"));
6
+ export const LEGACY_ARTIFACT_DIR = "vbrief";
7
+ export const MIGRATED_ARTIFACT_DIR = "xbrief";
8
+ export const LEGACY_ARTIFACT_SUFFIX = ".vbrief.json";
9
+ export const MIGRATED_ARTIFACT_SUFFIX = ".xbrief.json";
10
+ export const LEGACY_INFO_ROOT_KEY = "vBRIEFInfo";
11
+ export const MIGRATED_INFO_ROOT_KEY = "xBRIEFInfo";
12
+ /** Item-only status value added in xBRIEF v0.8. */
13
+ export const V08_ITEM_STATUS_AUTO = "auto";
14
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,11 @@
1
+ /** Structured result of a legacy vbrief layout probe (#2108 / #2034). */
2
+ export interface LegacyVbriefLayoutDetection {
3
+ legacyLayout: boolean;
4
+ reasons: string[];
5
+ }
6
+ /**
7
+ * Detect whether `projectRoot` still uses the legacy vbrief on-disk layout.
8
+ * Mirrors the structured-reasons pattern from `detectPreCutover` (#793).
9
+ */
10
+ export declare function detectLegacyVbriefLayout(projectRoot: string): LegacyVbriefLayoutDetection;
11
+ //# sourceMappingURL=detect.d.ts.map