@deftai/directive-core 0.64.0 → 0.66.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.
Files changed (165) 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 +3 -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.js +17 -14
  23. package/dist/intake/markdown-scanners.d.ts +3 -1
  24. package/dist/intake/markdown-scanners.js +10 -3
  25. package/dist/intake/reconcile-issues.js +13 -4
  26. package/dist/layout/index.d.ts +2 -0
  27. package/dist/layout/index.js +2 -0
  28. package/dist/layout/resolve.d.ts +87 -0
  29. package/dist/layout/resolve.js +159 -0
  30. package/dist/lifecycle/lifecycle-hygiene.d.ts +1 -1
  31. package/dist/lifecycle/lifecycle-hygiene.js +8 -5
  32. package/dist/orchestration/judgment-policy.js +4 -6
  33. package/dist/orchestration/verify-judgment-gates.d.ts +1 -0
  34. package/dist/orchestration/verify-judgment-gates.js +4 -2
  35. package/dist/policy/autonomy.js +4 -6
  36. package/dist/policy/capacity.js +4 -6
  37. package/dist/policy/decisions.d.ts +1 -0
  38. package/dist/policy/decisions.js +4 -2
  39. package/dist/policy/index.d.ts +1 -0
  40. package/dist/policy/index.js +3 -1
  41. package/dist/policy/plan-extensions.d.ts +35 -0
  42. package/dist/policy/plan-extensions.js +57 -0
  43. package/dist/policy/resolve.d.ts +6 -2
  44. package/dist/policy/resolve.js +18 -8
  45. package/dist/policy/wip.js +6 -4
  46. package/dist/preflight-cache/evaluate.js +11 -45
  47. package/dist/release/native-steps.js +4 -3
  48. package/dist/render/framework-commands.js +2 -1
  49. package/dist/render/prd-render.js +2 -2
  50. package/dist/render/project-render.js +23 -5
  51. package/dist/render/roadmap-render.js +36 -8
  52. package/dist/render/scope-outlook.js +3 -2
  53. package/dist/scope/audit-log.js +3 -2
  54. package/dist/scope/capacity-stamp.js +2 -1
  55. package/dist/scope/decompose.js +6 -4
  56. package/dist/scope/decomposed-refs.js +2 -1
  57. package/dist/scope/demote.js +5 -4
  58. package/dist/scope/project-context.js +2 -1
  59. package/dist/scope/project-definition-sync.js +4 -3
  60. package/dist/scope/transition.js +6 -2
  61. package/dist/scope/undo.js +2 -1
  62. package/dist/scope/vbrief-ref.js +5 -3
  63. package/dist/session/resume-conditions.js +2 -1
  64. package/dist/session/ritual-sentinel.js +2 -1
  65. package/dist/session/staleness.js +2 -1
  66. package/dist/slice/record.js +4 -4
  67. package/dist/spec-authority/resolver.js +5 -5
  68. package/dist/swarm/complete-cohort.js +5 -4
  69. package/dist/swarm/launch.js +6 -7
  70. package/dist/swarm/readiness.js +22 -20
  71. package/dist/swarm/subagent-backend.js +2 -5
  72. package/dist/swarm/verify-review-clean.js +2 -1
  73. package/dist/triage/actions/candidates-log.d.ts +3 -0
  74. package/dist/triage/actions/candidates-log.js +8 -2
  75. package/dist/triage/bootstrap/gitignore.js +22 -13
  76. package/dist/triage/bootstrap/index.js +7 -4
  77. package/dist/triage/bulk/index.js +2 -1
  78. package/dist/triage/classify/index.js +13 -11
  79. package/dist/triage/queue/audit.js +3 -3
  80. package/dist/triage/queue/build-queue.js +6 -0
  81. package/dist/triage/queue/cache.js +12 -2
  82. package/dist/triage/queue/index.d.ts +1 -0
  83. package/dist/triage/queue/index.js +1 -0
  84. package/dist/triage/queue/ranking-labels.js +2 -1
  85. package/dist/triage/queue/scope-ignores-filter.d.ts +12 -0
  86. package/dist/triage/queue/scope-ignores-filter.js +50 -0
  87. package/dist/triage/queue/scope-walk.js +8 -6
  88. package/dist/triage/queue/types.d.ts +4 -0
  89. package/dist/triage/reconcile/audit.js +2 -1
  90. package/dist/triage/reconcile/parse-uri.js +2 -1
  91. package/dist/triage/reconcile/reconcile.js +8 -7
  92. package/dist/triage/refresh/extract.js +4 -2
  93. package/dist/triage/refresh/refresh.js +3 -2
  94. package/dist/triage/scope/mutations-core.js +13 -9
  95. package/dist/triage/scope/renderers.js +5 -3
  96. package/dist/triage/scope/resolve.js +6 -5
  97. package/dist/triage/scope/validate.js +3 -6
  98. package/dist/triage/scope-drift/add-ignore.js +4 -2
  99. package/dist/triage/scope-drift/scope-rules.js +3 -2
  100. package/dist/triage/subscribe/index.js +14 -8
  101. package/dist/triage/summary/index.d.ts +33 -0
  102. package/dist/triage/summary/index.js +180 -6
  103. package/dist/triage/summary/reconcilable.js +6 -4
  104. package/dist/triage/summary/scope-drift.js +2 -1
  105. package/dist/triage/welcome/default-mode.d.ts +1 -0
  106. package/dist/triage/welcome/default-mode.js +1 -0
  107. package/dist/triage/welcome/prior-state.js +9 -9
  108. package/dist/triage/welcome/summary.d.ts +2 -0
  109. package/dist/triage/welcome/summary.js +15 -8
  110. package/dist/triage/welcome/writers.js +12 -10
  111. package/dist/validate-content/capacity-policy.js +5 -4
  112. package/dist/validate-content/capacity-show.js +5 -3
  113. package/dist/validate-content/filename.d.ts +1 -1
  114. package/dist/validate-content/filename.js +5 -4
  115. package/dist/validate-content/validate-strategy-output.js +14 -5
  116. package/dist/vbrief-build/build.js +1 -1
  117. package/dist/vbrief-build/constants.js +7 -0
  118. package/dist/vbrief-build/speckit.js +3 -2
  119. package/dist/vbrief-reconcile/graph.js +3 -2
  120. package/dist/vbrief-reconcile/labels.js +4 -3
  121. package/dist/vbrief-reconcile/reconciliation.js +5 -1
  122. package/dist/vbrief-reconcile/swarm-deps.js +6 -5
  123. package/dist/vbrief-reconcile/umbrellas.d.ts +1 -1
  124. package/dist/vbrief-reconcile/umbrellas.js +9 -7
  125. package/dist/vbrief-validate/conformance.d.ts +1 -2
  126. package/dist/vbrief-validate/conformance.js +29 -14
  127. package/dist/vbrief-validate/constants.d.ts +18 -1
  128. package/dist/vbrief-validate/constants.js +32 -3
  129. package/dist/vbrief-validate/epic-links.js +3 -2
  130. package/dist/vbrief-validate/filename.js +5 -3
  131. package/dist/vbrief-validate/index.d.ts +3 -1
  132. package/dist/vbrief-validate/index.js +2 -1
  133. package/dist/vbrief-validate/main.js +17 -6
  134. package/dist/vbrief-validate/origin.js +1 -1
  135. package/dist/vbrief-validate/paths.js +2 -1
  136. package/dist/vbrief-validate/plan-hooks.js +4 -3
  137. package/dist/vbrief-validate/registry-status.d.ts +1 -0
  138. package/dist/vbrief-validate/registry-status.js +3 -1
  139. package/dist/vbrief-validate/roundtrip.d.ts +36 -0
  140. package/dist/vbrief-validate/roundtrip.js +168 -0
  141. package/dist/vbrief-validate/schema.d.ts +1 -1
  142. package/dist/vbrief-validate/schema.js +62 -19
  143. package/dist/vbrief-validate/validate-all.js +12 -4
  144. package/dist/verify-source/code-structure-validate.js +8 -4
  145. package/dist/verify-source/contract-drift.d.ts +33 -0
  146. package/dist/verify-source/contract-drift.js +170 -0
  147. package/dist/verify-source/index.d.ts +1 -0
  148. package/dist/verify-source/index.js +1 -0
  149. package/dist/xbrief-migrate/constants.d.ts +13 -0
  150. package/dist/xbrief-migrate/constants.js +14 -0
  151. package/dist/xbrief-migrate/detect.d.ts +11 -0
  152. package/dist/xbrief-migrate/detect.js +81 -0
  153. package/dist/xbrief-migrate/drift-gate.d.ts +56 -0
  154. package/dist/xbrief-migrate/drift-gate.js +205 -0
  155. package/dist/xbrief-migrate/index.d.ts +7 -0
  156. package/dist/xbrief-migrate/index.js +7 -0
  157. package/dist/xbrief-migrate/migrate-project.d.ts +33 -0
  158. package/dist/xbrief-migrate/migrate-project.js +189 -0
  159. package/dist/xbrief-migrate/signpost.d.ts +5 -0
  160. package/dist/xbrief-migrate/signpost.js +19 -0
  161. package/dist/xbrief-migrate/transforms.d.ts +46 -0
  162. package/dist/xbrief-migrate/transforms.js +182 -0
  163. package/dist/xbrief-migrate/version.d.ts +8 -0
  164. package/dist/xbrief-migrate/version.js +25 -0
  165. package/package.json +11 -3
@@ -2,7 +2,9 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { GitCommandError, GitNotFoundError, gitStagedFiles, gitTrackedFiles, } from "../encoding/git.js";
4
4
  import { fnmatchCase } from "../encoding/text.js";
5
- export const DOC_CORE = new Set(["vBRIEFInfo", "plan"]);
5
+ import { isLifecycleArtifactPath, LIFECYCLE_DIR_NAMES } from "../layout/resolve.js";
6
+ import { evaluateExtensionRoundtrip } from "./roundtrip.js";
7
+ export const DOC_CORE = new Set(["vBRIEFInfo", "xBRIEFInfo", "plan"]);
6
8
  export const PLAN_CORE = new Set([
7
9
  "id",
8
10
  "uid",
@@ -30,11 +32,14 @@ export const PLAN_CORE = new Set([
30
32
  export const ITEM_CORE = new Set([
31
33
  "id",
32
34
  "uid",
35
+ "type",
36
+ "summary",
33
37
  "title",
34
38
  "status",
35
39
  "narrative",
36
40
  "subItems",
37
41
  "planRef",
42
+ "planRefs",
38
43
  "tags",
39
44
  "metadata",
40
45
  "created",
@@ -58,12 +63,11 @@ export const ITEM_CORE = new Set([
58
63
  "lockedBy",
59
64
  "items",
60
65
  ]);
61
- export const EXTENSION_PREFIXES = ["x-directive/", "x-vbrief/"];
62
- export const ALLOW_LIST = new Set(["plan.policy", "plan.completedNote"]);
66
+ export const EXTENSION_PREFIXES = ["x-directive/", "x-vbrief/", "x-xbrief/"];
63
67
  export function renderFinding(finding) {
64
68
  return ` ${finding.path} [${finding.level}] bare key '${finding.key}' at ${finding.location}`;
65
69
  }
66
- function isConformant(level, key, core) {
70
+ function isConformant(key, core) {
67
71
  if (core.has(key)) {
68
72
  return true;
69
73
  }
@@ -72,7 +76,8 @@ function isConformant(level, key, core) {
72
76
  return true;
73
77
  }
74
78
  }
75
- return ALLOW_LIST.has(`${level}.${key}`);
79
+ // No allow-list exceptions: every non-core key MUST be namespaced (#1650).
80
+ return false;
76
81
  }
77
82
  function planPlanRefFinding(relPath, value) {
78
83
  if (typeof value === "string" && value.trim().startsWith("#")) {
@@ -88,7 +93,7 @@ function planPlanRefFinding(relPath, value) {
88
93
  function scanItem(relPath, item, location) {
89
94
  const findings = [];
90
95
  for (const key of Object.keys(item)) {
91
- if (!isConformant("item", key, ITEM_CORE)) {
96
+ if (!isConformant(key, ITEM_CORE)) {
92
97
  findings.push({ path: relPath, level: "item", key, location });
93
98
  }
94
99
  }
@@ -113,7 +118,7 @@ export function scanVbrief(relPath, data) {
113
118
  }
114
119
  const doc = data;
115
120
  for (const key of Object.keys(doc)) {
116
- if (!isConformant("document", key, DOC_CORE)) {
121
+ if (!isConformant(key, DOC_CORE)) {
117
122
  findings.push({ path: relPath, level: "document", key, location: "<root>" });
118
123
  }
119
124
  }
@@ -130,7 +135,7 @@ export function scanVbrief(relPath, data) {
130
135
  }
131
136
  continue;
132
137
  }
133
- if (!isConformant("plan", key, PLAN_CORE)) {
138
+ if (!isConformant(key, PLAN_CORE)) {
134
139
  findings.push({ path: relPath, level: "plan", key, location: "plan" });
135
140
  }
136
141
  }
@@ -164,7 +169,8 @@ function isAllowListed(relPath, patterns) {
164
169
  return patterns.some((pat) => fnmatchCase(relPath, pat));
165
170
  }
166
171
  function isVbriefPath(posix) {
167
- return posix.startsWith("vbrief/") && posix.endsWith(".vbrief.json");
172
+ // Layout-aware (#2109 part 1): accept either lifecycle root + either suffix.
173
+ return isLifecycleArtifactPath(posix);
168
174
  }
169
175
  /** Pure driver returning exit code, findings, and human message. */
170
176
  export function evaluateConformance(projectRoot, options = {}) {
@@ -178,7 +184,7 @@ export function evaluateConformance(projectRoot, options = {}) {
178
184
  "(expected 'all' or 'staged').",
179
185
  };
180
186
  }
181
- if (!existsSync(join(root, "vbrief"))) {
187
+ if (!LIFECYCLE_DIR_NAMES.some((dir) => existsSync(join(root, dir)))) {
182
188
  return {
183
189
  exitCode: 2,
184
190
  findings: [],
@@ -251,8 +257,8 @@ export function evaluateConformance(projectRoot, options = {}) {
251
257
  const uniquePaths = new Set(findings.map((f) => f.path));
252
258
  const header = `\u274c verify_vbrief_conformance: detected ${findings.length} bare ` +
253
259
  `key(s) across ${uniquePaths.size} file(s) (#1620).\n` +
254
- " Every vBRIEF key MUST be 0.6 spec-core, x-directive/-namespaced, " +
255
- "or x-vbrief/-namespaced -- never bare.\n" +
260
+ " Every vBRIEF key MUST be spec-core, x-directive/-namespaced, " +
261
+ "x-vbrief/-namespaced, or x-xbrief/-namespaced -- never bare.\n" +
256
262
  " Fix: migrate misused/misspelled core fields to their core home " +
257
263
  "(see scripts/vbrief_migrate_conformance.py), or namespace a genuine\n" +
258
264
  " extension under x-directive/. Allow-list a documented file " +
@@ -263,11 +269,20 @@ export function evaluateConformance(projectRoot, options = {}) {
263
269
  }
264
270
  return { exitCode: 1, findings, message: `${header}\n${body}` };
265
271
  }
272
+ const extensionRoundtrip = evaluateExtensionRoundtrip(root);
273
+ if (extensionRoundtrip.exitCode !== 0) {
274
+ return {
275
+ exitCode: extensionRoundtrip.exitCode,
276
+ findings,
277
+ message: extensionRoundtrip.message,
278
+ };
279
+ }
280
+ const bareKeysMessage = `\u2713 verify_vbrief_conformance: ${candidates.length} vBRIEF file(s) ` +
281
+ "clean -- no bare keys (#1620).";
266
282
  return {
267
283
  exitCode: 0,
268
284
  findings,
269
- message: `\u2713 verify_vbrief_conformance: ${candidates.length} vBRIEF file(s) ` +
270
- "clean -- no bare keys (#1620).",
285
+ message: [extensionRoundtrip.message, bareKeysMessage].filter(Boolean).join("\n"),
271
286
  };
272
287
  }
273
288
  //# sourceMappingURL=conformance.js.map
@@ -1,12 +1,29 @@
1
- /** v0.6 Status enum from the canonical schema. */
1
+ /** v0.6/v0.8 plan-level Status enum (excludes item-only `auto`). */
2
+ export declare const VALID_PLAN_STATUSES: Set<string>;
3
+ /** PlanItem status enum — plan statuses plus container rollup `auto` (#2107 / xBRIEF v0.8). */
4
+ export declare const VALID_ITEM_STATUSES: Set<string>;
5
+ /** @deprecated Use VALID_PLAN_STATUSES or VALID_ITEM_STATUSES; kept for module re-exports. */
2
6
  export declare const VALID_STATUSES: Set<string>;
3
7
  export declare const VALID_VBRIEF_VERSIONS: Set<string>;
8
+ /** Accepted top-level document info blocks (additive v0.6 + v0.8). */
9
+ export declare const VALID_INFO_ROOT_KEYS: Set<string>;
10
+ /** v0.8 PlanItem.type enum values (optional field). */
11
+ export declare const VALID_PLAN_ITEM_TYPES: Set<string>;
4
12
  /** D13: status-to-folder mapping (#533 adds ``failed`` in completed/). */
5
13
  export declare const FOLDER_ALLOWED_STATUSES: Readonly<Record<string, ReadonlySet<string>>>;
6
14
  export declare const LIFECYCLE_FOLDERS: string[];
7
15
  /** D3: expected narrative keys for PROJECT-DEFINITION (normalized). */
8
16
  export declare const PROJECT_DEF_EXPECTED_NARRATIVES: Set<string>;
9
17
  export declare const STRICT_ORIGIN_ALLOWLIST: Set<string>;
18
+ /**
19
+ * Scope plan-link reference types under both the legacy `x-vbrief/` and the
20
+ * migrated `x-xbrief/` namespaces (#2109). Cross-file link validation (D4) and
21
+ * registry-status filtering must recognize both so a migrated corpus and a
22
+ * legacy corpus validate identically.
23
+ */
24
+ export declare const PLAN_REFERENCE_TYPES: Set<string>;
25
+ /** True when `value` is a scope plan-link reference type under either namespace. */
26
+ export declare function isPlanReferenceType(value: unknown): boolean;
10
27
  export declare const LEGACY_ORIGIN_TYPES: Set<string>;
11
28
  export declare const DEPRECATED_FILES: readonly ["SPECIFICATION.md", "PROJECT.md"];
12
29
  export declare const USAGE = "Usage: vbrief_validate.py [--vbrief-dir <path>] [--strict-origin-types] [--warnings-as-errors]";
@@ -1,5 +1,5 @@
1
- /** v0.6 Status enum from the canonical schema. */
2
- export const VALID_STATUSES = new Set([
1
+ /** v0.6/v0.8 plan-level Status enum (excludes item-only `auto`). */
2
+ export const VALID_PLAN_STATUSES = new Set([
3
3
  "draft",
4
4
  "proposed",
5
5
  "approved",
@@ -10,7 +10,15 @@ export const VALID_STATUSES = new Set([
10
10
  "failed",
11
11
  "cancelled",
12
12
  ]);
13
- export const VALID_VBRIEF_VERSIONS = new Set(["0.6"]);
13
+ /** PlanItem status enum plan statuses plus container rollup `auto` (#2107 / xBRIEF v0.8). */
14
+ export const VALID_ITEM_STATUSES = new Set([...VALID_PLAN_STATUSES, "auto"]);
15
+ /** @deprecated Use VALID_PLAN_STATUSES or VALID_ITEM_STATUSES; kept for module re-exports. */
16
+ export const VALID_STATUSES = VALID_PLAN_STATUSES;
17
+ export const VALID_VBRIEF_VERSIONS = new Set(["0.6", "0.8"]);
18
+ /** Accepted top-level document info blocks (additive v0.6 + v0.8). */
19
+ export const VALID_INFO_ROOT_KEYS = new Set(["vBRIEFInfo", "xBRIEFInfo"]);
20
+ /** v0.8 PlanItem.type enum values (optional field). */
21
+ export const VALID_PLAN_ITEM_TYPES = new Set(["task", "group", "milestone", "epic"]);
14
22
  /** D13: status-to-folder mapping (#533 adds ``failed`` in completed/). */
15
23
  export const FOLDER_ALLOWED_STATUSES = {
16
24
  proposed: new Set(["draft", "proposed"]),
@@ -29,7 +37,28 @@ export const STRICT_ORIGIN_ALLOWLIST = new Set([
29
37
  "x-vbrief/jira-ticket",
30
38
  "x-vbrief/user-request",
31
39
  "x-vbrief/spec-section",
40
+ "x-xbrief/plan",
41
+ "x-xbrief/github-issue",
42
+ "x-xbrief/github-pr",
43
+ "x-xbrief/jira-ticket",
44
+ "x-xbrief/user-request",
45
+ "x-xbrief/spec-section",
46
+ "x-xbrief/commit",
47
+ "x-xbrief/external",
48
+ "x-xbrief/research",
49
+ "x-xbrief/adr",
32
50
  ]);
51
+ /**
52
+ * Scope plan-link reference types under both the legacy `x-vbrief/` and the
53
+ * migrated `x-xbrief/` namespaces (#2109). Cross-file link validation (D4) and
54
+ * registry-status filtering must recognize both so a migrated corpus and a
55
+ * legacy corpus validate identically.
56
+ */
57
+ export const PLAN_REFERENCE_TYPES = new Set(["x-vbrief/plan", "x-xbrief/plan"]);
58
+ /** True when `value` is a scope plan-link reference type under either namespace. */
59
+ export function isPlanReferenceType(value) {
60
+ return typeof value === "string" && PLAN_REFERENCE_TYPES.has(value);
61
+ }
33
62
  export const LEGACY_ORIGIN_TYPES = new Set(["github-issue", "jira-ticket", "user-request"]);
34
63
  export const DEPRECATED_FILES = ["SPECIFICATION.md", "PROJECT.md"];
35
64
  export const USAGE = "Usage: vbrief_validate.py [--vbrief-dir <path>] [--strict-origin-types] [--warnings-as-errors]";
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { basename, resolve } from "node:path";
3
+ import { isPlanReferenceType } from "./constants.js";
3
4
  import { resolveRefPath } from "./paths.js";
4
5
  function collectPlanRefs(plan) {
5
6
  const refs = [];
@@ -77,7 +78,7 @@ export function validateEpicStoryLinks(allVbriefs, vbriefDir, resolvedToOriginal
77
78
  if (typeof uri !== "string" || !uri || typeof refType !== "string" || !refType) {
78
79
  continue;
79
80
  }
80
- if (refType !== "x-vbrief/plan") {
81
+ if (!isPlanReferenceType(refType)) {
81
82
  continue;
82
83
  }
83
84
  const childPath = resolveRefPath(uri, vbriefDir);
@@ -124,7 +125,7 @@ export function validateEpicStoryLinks(allVbriefs, vbriefDir, resolvedToOriginal
124
125
  if (typeof pref === "object" &&
125
126
  pref !== null &&
126
127
  !Array.isArray(pref) &&
127
- pref.type === "x-vbrief/plan") {
128
+ isPlanReferenceType(pref.type)) {
128
129
  const u = pref.uri;
129
130
  if (typeof u === "string") {
130
131
  childUris.add(u);
@@ -1,9 +1,11 @@
1
+ import { ARTIFACT_SUFFIXES, hasArtifactSuffix, stripArtifactSuffix } from "../layout/resolve.js";
1
2
  /** D7: filename convention without polynomial regex (#1782 s3). */
2
3
  export function matchesFilenameConvention(name) {
3
- if (!name.endsWith(".vbrief.json")) {
4
+ // Layout-aware (#2109 part 1): accept either .vbrief.json or .xbrief.json.
5
+ if (!ARTIFACT_SUFFIXES.some((suffix) => name.endsWith(suffix))) {
4
6
  return false;
5
7
  }
6
- const stem = name.slice(0, -".vbrief.json".length);
8
+ const stem = stripArtifactSuffix(name);
7
9
  if (stem.length < 12) {
8
10
  return false;
9
11
  }
@@ -48,7 +50,7 @@ export function matchesFilenameConvention(name) {
48
50
  /** Check filename matches YYYY-MM-DD-descriptive-slug.vbrief.json (D7). */
49
51
  export function validateFilename(filepath) {
50
52
  const name = filepath.split("/").pop() ?? filepath;
51
- if (name === "PROJECT-DEFINITION.vbrief.json") {
53
+ if (hasArtifactSuffix(name) && stripArtifactSuffix(name) === "PROJECT-DEFINITION") {
52
54
  return [];
53
55
  }
54
56
  if (!matchesFilenameConvention(name)) {
@@ -1,5 +1,5 @@
1
1
  export type { ConformanceEvaluateResult, ConformanceFinding, ConformanceMode, } from "./conformance.js";
2
- export { ALLOW_LIST, DOC_CORE, evaluateConformance, ITEM_CORE, PLAN_CORE, renderFinding, scanVbrief, } from "./conformance.js";
2
+ export { DOC_CORE, evaluateConformance, ITEM_CORE, PLAN_CORE, renderFinding, scanVbrief, } from "./conformance.js";
3
3
  export { LIFECYCLE_FOLDERS, USAGE, VALID_STATUSES } from "./constants.js";
4
4
  export { matchesFilenameConvention, validateFilename } from "./filename.js";
5
5
  export { validateFolderStatus } from "./folder-status.js";
@@ -8,6 +8,8 @@ export { cmdVbriefValidate, runConformance, runValidate } from "./main.js";
8
8
  export { validateOriginProvenance } from "./origin.js";
9
9
  export { validateSessionRitualStalenessHoursOnPlan, validateTriageRankingLabelsOnPlan, validateWipCapOnPlan, } from "./plan-hooks.js";
10
10
  export { validateProjectDefinition } from "./project-definition.js";
11
+ export type { ExtensionEntry, ExtensionRoundtripEvaluateResult, ExtensionRoundtripFinding, } from "./roundtrip.js";
12
+ export { collectExtensionEntries, EXTENSION_CONFORMANCE_FIXTURES_DIR, evaluateExtensionRoundtrip, findExtensionPreservationViolations, reEmitVbriefArtifact, renderExtensionRoundtripFinding, VbriefSchemaValidationError, } from "./roundtrip.js";
11
13
  export { normalizeNarrativeKey, validateVbriefSchema } from "./schema.js";
12
14
  export type { ValidateAllResult } from "./validate-all.js";
13
15
  export { discoverVbriefs, validateAll, validateAllMigration, } from "./validate-all.js";
@@ -1,4 +1,4 @@
1
- export { ALLOW_LIST, DOC_CORE, evaluateConformance, ITEM_CORE, PLAN_CORE, renderFinding, scanVbrief, } from "./conformance.js";
1
+ export { DOC_CORE, evaluateConformance, ITEM_CORE, PLAN_CORE, renderFinding, scanVbrief, } from "./conformance.js";
2
2
  export { LIFECYCLE_FOLDERS, USAGE, VALID_STATUSES } from "./constants.js";
3
3
  export { matchesFilenameConvention, validateFilename } from "./filename.js";
4
4
  export { validateFolderStatus } from "./folder-status.js";
@@ -6,6 +6,7 @@ export { cmdVbriefValidate, runConformance, runValidate } from "./main.js";
6
6
  export { validateOriginProvenance } from "./origin.js";
7
7
  export { validateSessionRitualStalenessHoursOnPlan, validateTriageRankingLabelsOnPlan, validateWipCapOnPlan, } from "./plan-hooks.js";
8
8
  export { validateProjectDefinition } from "./project-definition.js";
9
+ export { collectExtensionEntries, EXTENSION_CONFORMANCE_FIXTURES_DIR, evaluateExtensionRoundtrip, findExtensionPreservationViolations, reEmitVbriefArtifact, renderExtensionRoundtripFinding, VbriefSchemaValidationError, } from "./roundtrip.js";
9
10
  export { normalizeNarrativeKey, validateVbriefSchema } from "./schema.js";
10
11
  export { discoverVbriefs, validateAll, validateAllMigration, } from "./validate-all.js";
11
12
  //# sourceMappingURL=index.js.map
@@ -1,11 +1,16 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import { resolveLifecycleLayout } from "../layout/resolve.js";
3
4
  import { evaluateConformance } from "./conformance.js";
4
5
  import { USAGE } from "./constants.js";
5
6
  import { validateAll } from "./validate-all.js";
6
7
  /** CLI entry for vbrief_validate.py parity. */
7
8
  export function runValidate(argv) {
8
- let vbriefDir = "vbrief";
9
+ // Layout-aware (#2109 part 1/2a): when --vbrief-dir is omitted, resolve the
10
+ // lifecycle layout root for --project-root (xbrief when migrated, else vbrief
11
+ // -- unchanged on today's repo). The prior false-green hardcoded the dir.
12
+ let vbriefDir = null;
13
+ let projectRoot = null;
9
14
  let strictOriginTypes = false;
10
15
  let warningsAsErrors = false;
11
16
  let i = 0;
@@ -15,6 +20,10 @@ export function runValidate(argv) {
15
20
  vbriefDir = argv[i + 1] ?? vbriefDir;
16
21
  i += 2;
17
22
  }
23
+ else if (arg === "--project-root" && i + 1 < argv.length) {
24
+ projectRoot = argv[i + 1] ?? projectRoot;
25
+ i += 2;
26
+ }
18
27
  else if (arg === "--strict-origin-types") {
19
28
  strictOriginTypes = true;
20
29
  i += 1;
@@ -33,11 +42,12 @@ export function runValidate(argv) {
33
42
  return 2;
34
43
  }
35
44
  }
36
- if (!existsSync(vbriefDir)) {
37
- process.stdout.write(`OK: No vbrief directory at ${vbriefDir} -- skipping validation\n`);
45
+ const resolvedDir = vbriefDir ?? resolveLifecycleLayout(projectRoot ?? process.cwd()).root;
46
+ if (!existsSync(resolvedDir)) {
47
+ process.stdout.write(`OK: No vbrief directory at ${resolvedDir} -- skipping validation\n`);
38
48
  return 0;
39
49
  }
40
- const { errors, warnings, scopeCount } = validateAll(vbriefDir, { strictOriginTypes });
50
+ const { errors, warnings, scopeCount } = validateAll(resolvedDir, { strictOriginTypes });
41
51
  for (const w of warnings) {
42
52
  process.stdout.write(`WARN: ${w}\n`);
43
53
  }
@@ -51,8 +61,9 @@ export function runValidate(argv) {
51
61
  if (scopeCount > 0) {
52
62
  parts.push(`${scopeCount} scope vBRIEF(s)`);
53
63
  }
54
- const projectDef = resolve(vbriefDir, "PROJECT-DEFINITION.vbrief.json");
55
- if (existsSync(projectDef)) {
64
+ const projectDef = resolve(resolvedDir, "PROJECT-DEFINITION.vbrief.json");
65
+ const projectDefXbrief = resolve(resolvedDir, "PROJECT-DEFINITION.xbrief.json");
66
+ if (existsSync(projectDef) || existsSync(projectDefXbrief)) {
56
67
  parts.push("PROJECT-DEFINITION");
57
68
  }
58
69
  const summary = parts.length > 0 ? parts.join(", ") : "no vBRIEF files";
@@ -12,7 +12,7 @@ function legacyOriginMatch(refType) {
12
12
  return false;
13
13
  }
14
14
  function schemaTrustingOrigin(refType) {
15
- return refType.startsWith("x-vbrief/");
15
+ return refType.startsWith("x-vbrief/") || refType.startsWith("x-xbrief/");
16
16
  }
17
17
  /** Warn if a scope vBRIEF in pending/ or active/ has no origin reference (D11). */
18
18
  export function validateOriginProvenance(filepath, data, vbriefDir, strictOriginTypes = false) {
@@ -1,4 +1,5 @@
1
1
  import { basename, relative, resolve, sep } from "node:path";
2
+ import { hasArtifactSuffix, stripArtifactSuffix } from "../layout/resolve.js";
2
3
  /** Return true when ``child`` resolves under ``parent``. */
3
4
  export function isRelativeTo(child, parent) {
4
5
  const resolvedParent = resolve(parent);
@@ -45,7 +46,7 @@ function splitMax(value, sep, maxsplit) {
45
46
  export function scopeIdsForRefUri(uri) {
46
47
  const rel = uri.startsWith("file://") ? uri.slice("file://".length) : uri;
47
48
  const name = basename(rel);
48
- const fullId = name.endsWith(".vbrief.json") ? name.slice(0, -".vbrief.json".length) : name;
49
+ const fullId = hasArtifactSuffix(name) ? stripArtifactSuffix(name) : name;
49
50
  const ids = new Set([fullId]);
50
51
  const parts = splitMax(fullId, "-", 3);
51
52
  if (parts.length === 4 &&
@@ -1,3 +1,4 @@
1
+ import { readPlanPolicy } from "../policy/plan-extensions.js";
1
2
  import { validateTriageAutoClassifyOnPlan, validateTriageHoldMarkersOnPlan, } from "../triage/classify/index.js";
2
3
  import { validateRankingLabels } from "../triage/queue/ranking-labels.js";
3
4
  import { pyStrRepr, pythonTypeName } from "../triage/scope/python-repr.js";
@@ -30,7 +31,7 @@ export function validateWipCapOnPlan(plan, filepath) {
30
31
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
31
32
  return [];
32
33
  }
33
- const policy = plan.policy;
34
+ const policy = readPlanPolicy(plan);
34
35
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
35
36
  return [];
36
37
  }
@@ -72,7 +73,7 @@ export function validateSessionRitualStalenessHoursOnPlan(plan, filepath) {
72
73
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
73
74
  return [];
74
75
  }
75
- const policy = plan.policy;
76
+ const policy = readPlanPolicy(plan);
76
77
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
77
78
  return [];
78
79
  }
@@ -90,7 +91,7 @@ export function validateTriageRankingLabelsOnPlan(plan, filepath) {
90
91
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
91
92
  return [];
92
93
  }
93
- const policy = plan.policy;
94
+ const policy = readPlanPolicy(plan);
94
95
  const raw = typeof policy === "object" && policy !== null && !Array.isArray(policy)
95
96
  ? policy.triageRankingLabels
96
97
  : undefined;
@@ -1,4 +1,5 @@
1
1
  import type { JsonObject } from "./schema.js";
2
+ /** @deprecated Use {@link isPlanReferenceType}; kept for back-compat re-exports. */
2
3
  export declare const REGISTRY_SCOPE_LINK_TYPE = "x-vbrief/plan";
3
4
  /** Derive the registry item status from a scope vBRIEF (render side). */
4
5
  export declare function deriveRegistryItemStatus(planStatus: unknown, lifecycleFolder: string): string;
@@ -1,3 +1,5 @@
1
+ import { isPlanReferenceType } from "./constants.js";
2
+ /** @deprecated Use {@link isPlanReferenceType}; kept for back-compat re-exports. */
1
3
  export const REGISTRY_SCOPE_LINK_TYPE = "x-vbrief/plan";
2
4
  /** Derive the registry item status from a scope vBRIEF (render side). */
3
5
  export function deriveRegistryItemStatus(planStatus, lifecycleFolder) {
@@ -10,7 +12,7 @@ function isScopeLinkRef(ref) {
10
12
  return (typeof ref === "object" &&
11
13
  ref !== null &&
12
14
  !Array.isArray(ref) &&
13
- ref.type === REGISTRY_SCOPE_LINK_TYPE);
15
+ isPlanReferenceType(ref.type));
14
16
  }
15
17
  /**
16
18
  * References copied onto a registry item from a scope vBRIEF.
@@ -0,0 +1,36 @@
1
+ import type { JsonObject } from "./schema.js";
2
+ /** Relative path to extension round-trip conformance fixtures (#715). */
3
+ export declare const EXTENSION_CONFORMANCE_FIXTURES_DIR = "content/vbrief/conformance/extensions/valid";
4
+ export interface ExtensionEntry {
5
+ readonly jsonPath: string;
6
+ readonly key: string;
7
+ readonly value: unknown;
8
+ }
9
+ export interface ExtensionRoundtripFinding {
10
+ readonly path: string;
11
+ readonly message: string;
12
+ }
13
+ /** Thrown when `reEmitVbriefArtifact` rejects an artifact that fails schema validation. */
14
+ export declare class VbriefSchemaValidationError extends Error {
15
+ readonly relPath: string;
16
+ readonly errors: readonly string[];
17
+ constructor(relPath: string, errors: readonly string[]);
18
+ }
19
+ /** Recursively collect every `x-<consumer>/` key in `value` using the contract pattern. */
20
+ export declare function collectExtensionEntries(value: unknown, jsonPath?: string): ExtensionEntry[];
21
+ /** Compare extension keys/values between two parsed artifacts (JSON structural equality). */
22
+ export declare function findExtensionPreservationViolations(original: unknown, roundtripped: unknown): string[];
23
+ /**
24
+ * Canonical reference-consumer read/write pipeline: validate, serialize, re-parse.
25
+ * Extension properties MUST survive this path verbatim (xBRIEF v0.8 §7).
26
+ */
27
+ export declare function reEmitVbriefArtifact(artifact: JsonObject, relPath?: string): JsonObject;
28
+ export declare function renderExtensionRoundtripFinding(finding: ExtensionRoundtripFinding): string;
29
+ export interface ExtensionRoundtripEvaluateResult {
30
+ readonly exitCode: number;
31
+ readonly findings: readonly ExtensionRoundtripFinding[];
32
+ readonly message: string;
33
+ }
34
+ /** Run extension round-trip preservation over packaged conformance fixtures. */
35
+ export declare function evaluateExtensionRoundtrip(projectRoot: string): ExtensionRoundtripEvaluateResult;
36
+ //# sourceMappingURL=roundtrip.d.ts.map
@@ -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;