@deftai/directive-core 0.107.0 → 0.108.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 (167) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/check/cached-orchestrator.js +28 -3
  3. package/dist/check/named-cause.js +18 -3
  4. package/dist/codebase/provider.js +7 -1
  5. package/dist/delivery-attempt/handoff.js +1 -1
  6. package/dist/design-critique/citation-grammar.d.ts +53 -0
  7. package/dist/design-critique/citation-grammar.js +303 -0
  8. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  9. package/dist/design-critique/completed-arc-record.js +232 -0
  10. package/dist/freshness/bind.d.ts +32 -3
  11. package/dist/freshness/bind.js +58 -12
  12. package/dist/hooks/classify/host-session-identity.d.ts +76 -0
  13. package/dist/hooks/classify/host-session-identity.js +470 -0
  14. package/dist/hooks/classify/index.d.ts +3 -2
  15. package/dist/hooks/classify/index.js +3 -2
  16. package/dist/hooks/classify/paths.d.ts +7 -0
  17. package/dist/hooks/classify/paths.js +35 -0
  18. package/dist/hooks/classify/stdin.d.ts +5 -0
  19. package/dist/hooks/classify/stdin.js +27 -0
  20. package/dist/hooks/dispatcher.d.ts +52 -1
  21. package/dist/hooks/dispatcher.js +538 -45
  22. package/dist/hooks/fixtures/cases.d.ts +16 -2
  23. package/dist/hooks/fixtures/cases.js +175 -0
  24. package/dist/hooks/scope.js +3 -1
  25. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  26. package/dist/init-deposit/agent-hooks.js +8 -1
  27. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  28. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  29. package/dist/intake/clause-derivation.js +9 -3
  30. package/dist/intake/issue-ingest.js +62 -2
  31. package/dist/intake/platform-capabilities.d.ts +9 -2
  32. package/dist/intake/platform-capabilities.js +38 -9
  33. package/dist/orphan-active/evaluate.d.ts +25 -0
  34. package/dist/orphan-active/evaluate.js +193 -128
  35. package/dist/orphan-active/index.d.ts +1 -0
  36. package/dist/orphan-active/index.js +1 -0
  37. package/dist/orphan-active/issue-state.d.ts +121 -0
  38. package/dist/orphan-active/issue-state.js +278 -0
  39. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  40. package/dist/platform/cursor-managed-runtime.js +241 -0
  41. package/dist/platform/index.d.ts +1 -0
  42. package/dist/platform/index.js +1 -0
  43. package/dist/platform/platform-capabilities.d.ts +23 -1
  44. package/dist/platform/platform-capabilities.js +55 -15
  45. package/dist/policy/ceremony-dial.js +5 -9
  46. package/dist/policy/host-hooks.js +4 -9
  47. package/dist/policy/org-force-on-migration.js +5 -10
  48. package/dist/policy/plan-extensions.d.ts +14 -2
  49. package/dist/policy/plan-extensions.js +24 -3
  50. package/dist/policy/product-signal.js +5 -10
  51. package/dist/policy/require-human-merge.js +5 -9
  52. package/dist/policy/resolve.js +77 -16
  53. package/dist/policy/value-feedback.js +9 -18
  54. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  55. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  56. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  57. package/dist/pr-closeout-attestable/index.js +2 -0
  58. package/dist/pr-protected-issues/gh.d.ts +6 -2
  59. package/dist/pr-protected-issues/gh.js +7 -3
  60. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  61. package/dist/pr-wait-mergeable/cascade.js +21 -1
  62. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  63. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  64. package/dist/pr-wait-mergeable/wrappers.js +11 -0
  65. package/dist/preflight/evaluate.d.ts +15 -0
  66. package/dist/preflight/evaluate.js +29 -3
  67. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  68. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  69. package/dist/product-first-done-gate/evaluate.js +17 -7
  70. package/dist/release/cli-drift-report.d.ts +68 -0
  71. package/dist/release/cli-drift-report.js +189 -0
  72. package/dist/release/index.d.ts +1 -0
  73. package/dist/release/index.js +1 -0
  74. package/dist/release/issue-state-fetch.d.ts +5 -3
  75. package/dist/release/issue-state-fetch.js +86 -21
  76. package/dist/release/native-steps.js +1 -0
  77. package/dist/release/pipeline.js +19 -0
  78. package/dist/release/types.d.ts +14 -0
  79. package/dist/render/constants.d.ts +4 -0
  80. package/dist/render/constants.js +11 -8
  81. package/dist/render/export-spec.js +31 -5
  82. package/dist/render/index.d.ts +1 -1
  83. package/dist/render/index.js +1 -1
  84. package/dist/render/prd-render.d.ts +4 -1
  85. package/dist/render/prd-render.js +82 -23
  86. package/dist/render/project-render.js +14 -7
  87. package/dist/render/scope-outlook.d.ts +2 -0
  88. package/dist/render/scope-outlook.js +3 -0
  89. package/dist/render/spec-render.js +2 -2
  90. package/dist/resolution/package-manager.d.ts +33 -4
  91. package/dist/resolution/package-manager.js +210 -9
  92. package/dist/review-monitor/constants.d.ts +7 -0
  93. package/dist/review-monitor/constants.js +7 -0
  94. package/dist/review-monitor/github-lease.d.ts +98 -1
  95. package/dist/review-monitor/github-lease.js +182 -1
  96. package/dist/review-monitor/lease-comment.d.ts +36 -0
  97. package/dist/review-monitor/lease-comment.js +103 -2
  98. package/dist/scm/call.js +12 -3
  99. package/dist/scm/design-critique-chip.d.ts +1 -0
  100. package/dist/scm/design-critique-chip.js +22 -6
  101. package/dist/scm/gh-rest.d.ts +9 -1
  102. package/dist/scm/gh-rest.js +103 -0
  103. package/dist/scm/readiness.d.ts +2 -0
  104. package/dist/scm/readiness.js +28 -4
  105. package/dist/scope/project-definition-sync.js +14 -7
  106. package/dist/session/git.d.ts +24 -0
  107. package/dist/session/git.js +138 -2
  108. package/dist/session/occupancy.d.ts +238 -12
  109. package/dist/session/occupancy.js +815 -29
  110. package/dist/session/ritual-sentinel.d.ts +5 -0
  111. package/dist/session/ritual-sentinel.js +5 -0
  112. package/dist/session/session-ready.d.ts +5 -1
  113. package/dist/session/session-ready.js +98 -8
  114. package/dist/session/session-start.d.ts +5 -1
  115. package/dist/session/session-start.js +32 -17
  116. package/dist/session/toolchain-preflight.d.ts +13 -3
  117. package/dist/session/toolchain-preflight.js +93 -18
  118. package/dist/session/verify-session-ritual.d.ts +11 -0
  119. package/dist/session/verify-session-ritual.js +60 -15
  120. package/dist/slice/constants.d.ts +1 -1
  121. package/dist/spec-authority/constants.d.ts +10 -2
  122. package/dist/spec-authority/constants.js +53 -8
  123. package/dist/spec-authority/resolver.d.ts +3 -0
  124. package/dist/spec-authority/resolver.js +55 -7
  125. package/dist/subprocess/max-buffer.d.ts +14 -0
  126. package/dist/subprocess/max-buffer.js +15 -0
  127. package/dist/swarm/complete-cohort.d.ts +2 -0
  128. package/dist/swarm/complete-cohort.js +21 -13
  129. package/dist/swarm/index.d.ts +1 -0
  130. package/dist/swarm/index.js +1 -0
  131. package/dist/swarm/launch-cli.js +53 -33
  132. package/dist/swarm/launch.d.ts +8 -0
  133. package/dist/swarm/launch.js +63 -43
  134. package/dist/swarm/pre-dispatch-cli.js +2 -1
  135. package/dist/swarm/pre-dispatch.js +10 -1
  136. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  137. package/dist/swarm/subagent-status-dir.js +42 -0
  138. package/dist/swarm/worktrees.d.ts +17 -3
  139. package/dist/swarm/worktrees.js +48 -4
  140. package/dist/triage/scope/mutations-core.d.ts +2 -6
  141. package/dist/triage/scope/mutations-core.js +23 -55
  142. package/dist/triage/scope-drift/add-ignore.js +5 -39
  143. package/dist/triage/subscribe/index.d.ts +2 -6
  144. package/dist/triage/subscribe/index.js +15 -65
  145. package/dist/triage/welcome/writers.js +15 -74
  146. package/dist/vbrief-build/index.d.ts +2 -1
  147. package/dist/vbrief-build/index.js +2 -1
  148. package/dist/vbrief-build/parity-scenarios.js +5 -5
  149. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  150. package/dist/vbrief-build/project-definition-io.js +357 -31
  151. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  152. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  153. package/dist/vbrief-validate/conformance.d.ts +1 -0
  154. package/dist/vbrief-validate/conformance.js +85 -8
  155. package/dist/vbrief-validate/main.js +2 -0
  156. package/dist/vbrief-validate/precutover.js +5 -10
  157. package/dist/verify-ac/clauses.d.ts +62 -2
  158. package/dist/verify-ac/clauses.js +183 -111
  159. package/dist/verify-ac/index.d.ts +1 -1
  160. package/dist/verify-ac/index.js +1 -1
  161. package/dist/verify-env/node-runtime.d.ts +8 -4
  162. package/dist/verify-env/node-runtime.js +9 -6
  163. package/dist/verify-env/toolchain-check.d.ts +35 -3
  164. package/dist/verify-env/toolchain-check.js +155 -36
  165. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  166. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  167. package/package.json +7 -3
@@ -16,6 +16,7 @@ import { spawnSync } from "node:child_process";
16
16
  import { existsSync, readFileSync, statSync } from "node:fs";
17
17
  import { basename, extname, join, resolve } from "node:path";
18
18
  import { hasArtifactSuffix, LIFECYCLE_DIR_NAMES } from "../layout/resolve.js";
19
+ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
19
20
  // ---------------------------------------------------------------------------
20
21
  // Classification constants
21
22
  // ---------------------------------------------------------------------------
@@ -776,6 +777,7 @@ function gitDiff(projectRoot, baseRef) {
776
777
  cwd: resolve(projectRoot),
777
778
  encoding: "utf8",
778
779
  timeout: 30_000,
780
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
779
781
  });
780
782
  if (result.error) {
781
783
  return [
@@ -6,6 +6,7 @@ import { readCorePackageVersion } from "../engine-version.js";
6
6
  import { applyProductFirstGateMode, EMPTY_AC_CAUSE, EMPTY_AC_REMEDY, isHygieneGate, isProductAcGate, isSoftEmptyAcText, resolveProductFirstCheckMode, } from "../product-first-done-gate/index.js";
7
7
  import { RunSummaryEmitter } from "../run-summary/emit.js";
8
8
  import { runToolchainPreflight, SKIP_ALL_GATES, } from "../session/toolchain-preflight.js";
9
+ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
9
10
  import { checkGateCliArgv, cliSpawnPlan, resolveGateDispatch, resolveGlobalCliBin, } from "./cli-native-gates.js";
10
11
  import { evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, } from "./consumer-gate-integrity.js";
11
12
  import { resolveCheckTarget } from "./context.js";
@@ -17,6 +18,7 @@ function captureSpawn(taskBin, args, opts) {
17
18
  cwd: opts.cwd,
18
19
  encoding: "utf8",
19
20
  env: opts.env ?? process.env,
21
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
20
22
  });
21
23
  if (result.error !== undefined) {
22
24
  return {
@@ -40,6 +42,27 @@ function writeLines(lines, stream = "stderr") {
40
42
  write(`${line}\n`);
41
43
  }
42
44
  }
45
+ function firstImpactingMissingFinding(preflight) {
46
+ const missing = preflight?.findings.filter((finding) => !finding.present) ?? [];
47
+ const impacting = missing.filter((finding) => finding.impact !== "none");
48
+ const priority = [
49
+ "node",
50
+ "task",
51
+ "cli_dist",
52
+ "package_manager",
53
+ preflight?.packageManager,
54
+ "npm",
55
+ "pnpm",
56
+ ];
57
+ for (const tool of priority) {
58
+ if (tool === null || tool === undefined)
59
+ continue;
60
+ const finding = impacting.find((candidate) => candidate.tool === tool);
61
+ if (finding !== undefined)
62
+ return finding;
63
+ }
64
+ return impacting[0] ?? missing[0];
65
+ }
43
66
  /**
44
67
  * Run check gates sequentially with content-hash caching (#1713).
45
68
  * Falls back to fail-open execution for undeclared / non-cacheable gates.
@@ -135,6 +158,7 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
135
158
  composedGates: gates,
136
159
  consumerDeposit: target === "check:consumer",
137
160
  which,
161
+ env: options.env,
138
162
  })
139
163
  : options.preflight;
140
164
  const taskPresent = preflight === null || !preflight.findings.some((f) => f.tool === "task" && !f.present);
@@ -165,8 +189,9 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
165
189
  if (allSkipped) {
166
190
  const skipped = gates.map((g) => {
167
191
  const id = checkGateId(g);
168
- const cause = preflight?.findings.find((f) => !f.present)?.cause ?? "toolchain preflight degraded";
169
- const remedy = preflight?.findings.find((f) => !f.present)?.remedy ?? remedyForGate(id, cause);
192
+ const missing = firstImpactingMissingFinding(preflight);
193
+ const cause = missing?.cause ?? "toolchain preflight degraded";
194
+ const remedy = missing?.remedy ?? remedyForGate(id, cause);
170
195
  gateOutcomes.push({ id, status: "skipped", cause, remedy });
171
196
  return { id, cause, remedy };
172
197
  });
@@ -183,7 +208,7 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
183
208
  const gateId = checkGateId(gateSpec);
184
209
  // #3282: skip gates that require missing tools (e.g. pnpm-only suite).
185
210
  if (skipSet.has(gateId)) {
186
- const missing = preflight?.findings.find((f) => !f.present);
211
+ const missing = firstImpactingMissingFinding(preflight);
187
212
  const cause = missing?.cause ?? "toolchain preflight marked gate skippable";
188
213
  const remedy = missing?.remedy ?? remedyForGate(gateId, cause);
189
214
  process.stderr.write(`check: skipping gate ${gateId} (degraded) — cause: ${cause}; remedy: ${remedy}\n`);
@@ -13,7 +13,7 @@ const GATE_REMEDIES = {
13
13
  "verify:wip-cap": "Demote stale pending scopes (task scope:demote) or raise plan.policy.wipCap deliberately",
14
14
  doctor: "Run task doctor and follow the named recovery steps",
15
15
  "toolchain:check": "Install missing maintainer tools reported by the gate (go, uv, git, gh, node, pnpm)",
16
- "toolchain:check-consumer": "Install missing consumer tools: go-task, git, gh, node, pnpm (corepack enable && corepack prepare pnpm@latest --activate)",
16
+ "toolchain:check-consumer": "Install the missing consumer tool reported by the gate; use npm from Node or enable pnpm with Corepack as declared by package.json",
17
17
  "ts:check-lane": "Fix lint/type/test failures; re-run task ts:check-lane",
18
18
  "vbrief:validate": "Fix xBRIEF/vBRIEF schema errors reported by the gate",
19
19
  "verify-strategy-output": "Re-run strategy output or fix non-conformant scope filenames / PROJECT-DEFINITION",
@@ -57,6 +57,12 @@ export function extractGateCause(stdout, stderr, exitCode, spawnError, gateId) {
57
57
  useful.push(line);
58
58
  }
59
59
  const gateHint = gateId?.trim() ?? "";
60
+ if (gateHint === "toolchain:check" || gateHint === "toolchain:check-consumer") {
61
+ const toolFailure = useful.find((line) => /^(?:package manager|go|uv|git|gh|node|npm|pnpm|task): (?:NOT FOUND|FAILED|ERROR)\b/i.test(line));
62
+ if (toolFailure !== undefined) {
63
+ return sanitizeCauseLine(toolFailure);
64
+ }
65
+ }
60
66
  if (gateHint.length > 0) {
61
67
  const named = useful.find((line) => line.includes(gateHint));
62
68
  if (named !== undefined) {
@@ -106,14 +112,23 @@ export function remedyForGate(gateId, cause) {
106
112
  return CLI_SPAWN_ERROR_REMEDY;
107
113
  }
108
114
  if (/task binary not found|cannot spawn go-task/i.test(cause)) {
109
- if (gateId.startsWith("verify:") || gateId.startsWith("verify-") || gateId === "doctor") {
115
+ if (gateId.startsWith("verify:") ||
116
+ gateId.startsWith("verify-") ||
117
+ gateId === "doctor" ||
118
+ gateId === "toolchain:check-consumer") {
110
119
  return CLI_SPAWN_ERROR_REMEDY;
111
120
  }
112
121
  return SPAWN_ERROR_REMEDY;
113
122
  }
114
- if (/pnpm binary not found|pnpm: NOT FOUND/i.test(cause)) {
123
+ if (/Unsupported DEFT_PACKAGE_MANAGER value/i.test(cause)) {
124
+ return "Set DEFT_PACKAGE_MANAGER to npm or pnpm, or unset it to use package.json; then re-run the consumer check";
125
+ }
126
+ if (/\bpnpm(?: binary not found|: (?:NOT FOUND|FAILED|ERROR))/i.test(cause)) {
115
127
  return "Enable pnpm: corepack enable && corepack prepare pnpm@latest --activate";
116
128
  }
129
+ if (/\bnpm(?: binary not found|: (?:NOT FOUND|FAILED|ERROR))/i.test(cause)) {
130
+ return "Install or repair Node 20+ (npm is bundled), then re-run the consumer check";
131
+ }
117
132
  return (GATE_REMEDIES[gateId] ??
118
133
  `Re-run the gate for details: task ${gateId} (or task check); fix the reported product/process defect`);
119
134
  }
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { contentRoot } from "../content-root.js";
7
7
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
8
8
  import { readPlanPolicy } from "../policy/plan-extensions.js";
9
+ import { resolveCaptureFailureStderr, SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
9
10
  import { loadJsonFile } from "../verify-source/code-structure-validate.js";
10
11
  import { CODEBASE_MAP_SCHEMA_PATH } from "./constants.js";
11
12
  import { buildCodebaseMap, CodeStructureConfigError, configErrorToDict, defaultCodeStructurePath, fileSha256, } from "./default-extractor.js";
@@ -547,6 +548,7 @@ function runProviderCommand(command, cwd) {
547
548
  cwd,
548
549
  encoding: "utf8",
549
550
  timeout: 60_000,
551
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
550
552
  stdio: ["ignore", "pipe", "pipe"],
551
553
  });
552
554
  return { returncode: 0, stdout, stderr: "" };
@@ -556,7 +558,11 @@ function runProviderCommand(command, cwd) {
556
558
  return {
557
559
  returncode: typeof e.status === "number" ? e.status : 1,
558
560
  stdout: typeof e.stdout === "string" ? e.stdout : "",
559
- stderr: typeof e.stderr === "string" ? e.stderr : String(e.message ?? err),
561
+ stderr: resolveCaptureFailureStderr({
562
+ captured: typeof e.stderr === "string" ? e.stderr : "",
563
+ status: e.status,
564
+ message: e.message ?? String(err),
565
+ }),
560
566
  };
561
567
  }
562
568
  }
@@ -8,7 +8,7 @@ import { utcIso } from "./types.js";
8
8
  export function nextSafeActionFor(decision) {
9
9
  switch (decision) {
10
10
  case "DENY_DUPLICATE_ACTIVE":
11
- return "Wait for the active attempt to finish or cancel it; do not start a duplicate dispatch.";
11
+ return "A running attempt is not proof the worker is alive. Takeover: swarm:pre-dispatch --action cancel, then begin. REDISPATCH_OK does not lift this deny.";
12
12
  case "BLOCK_NON_RETRYABLE":
13
13
  return "Fix the deterministic configuration/schema/permission failure, record a relevant material delta, then resume.";
14
14
  case "BLOCK_NO_MATERIAL_PROGRESS":
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Citation grammar for the design-critique completed-arc record (#3831).
3
+ *
4
+ * One parser answers both questions the ingest predicate asks: which comment
5
+ * ids a synthesis cites, and whether it claims a verified-claims table. The
6
+ * closed accepted set and the refused positions are published in
7
+ * `content/contracts/design-critique.md` `## Citation grammar`; this module is
8
+ * the only implementation of that grammar.
9
+ *
10
+ * Which code-span convention governs. The intake cross-ref scanners
11
+ * (`intake/markdown-scanners.ts`) delete code spans, so for them a backticked
12
+ * id is an example and never a reference. The citation scan takes the opposite
13
+ * polarity for the id token only: a balanced single-backtick id is an accepted
14
+ * citation, because arc comments are hand-written prose and the mandated lean
15
+ * heading is itself `**Lean:**`. A keyword inside a code span or a fence stays
16
+ * an example in both layers. Positions are classified, never stripped -- the
17
+ * strippers delete a span with its contents, which destroys the digits. The
18
+ * prior art for classify-not-strip is `classifyHit`
19
+ * (`pr-closing-keywords/detect.ts`).
20
+ *
21
+ * The fence scan is local rather than borrowed from that detector because the
22
+ * detector's opener regex is anchored at column zero, so a CommonMark fence
23
+ * indented one to three spaces reads as prose. Relaxing it there would move
24
+ * hits into the #737 false-positive class and change what that gate suppresses,
25
+ * which is a separate decision from this one.
26
+ */
27
+ export type CitationKind = "lean" | "table" | "comment";
28
+ export type CitationRejectionClass = "code-fence" | "inline-code" | "blockquote" | "strikethrough" | "negation";
29
+ export type Citation = {
30
+ readonly id: number;
31
+ readonly kind: CitationKind;
32
+ };
33
+ export type RejectedCitation = {
34
+ readonly id: number;
35
+ readonly reason: CitationRejectionClass;
36
+ };
37
+ export type CitationScan = {
38
+ /** Accepted citations in document order, deduplicated on (id, kind). */
39
+ readonly citations: readonly Citation[];
40
+ /** Keyword-anchored or permalink occurrences refused by position. */
41
+ readonly rejected: readonly RejectedCitation[];
42
+ /** Every 8-or-more digit run in the body, for deterministic diagnostics. */
43
+ readonly idShapedRuns: readonly number[];
44
+ };
45
+ /** The published closed set, in the order the contract lists it. */
46
+ export declare const ACCEPTED_CITATION_FORMS: readonly string[];
47
+ /**
48
+ * Parse the closed citation grammar out of one comment body. Refused positions
49
+ * are reported rather than dropped so a block detail can echo the observation
50
+ * instead of guessing at a cause.
51
+ */
52
+ export declare function scanCitations(body: string): CitationScan;
53
+ //# sourceMappingURL=citation-grammar.d.ts.map
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Citation grammar for the design-critique completed-arc record (#3831).
3
+ *
4
+ * One parser answers both questions the ingest predicate asks: which comment
5
+ * ids a synthesis cites, and whether it claims a verified-claims table. The
6
+ * closed accepted set and the refused positions are published in
7
+ * `content/contracts/design-critique.md` `## Citation grammar`; this module is
8
+ * the only implementation of that grammar.
9
+ *
10
+ * Which code-span convention governs. The intake cross-ref scanners
11
+ * (`intake/markdown-scanners.ts`) delete code spans, so for them a backticked
12
+ * id is an example and never a reference. The citation scan takes the opposite
13
+ * polarity for the id token only: a balanced single-backtick id is an accepted
14
+ * citation, because arc comments are hand-written prose and the mandated lean
15
+ * heading is itself `**Lean:**`. A keyword inside a code span or a fence stays
16
+ * an example in both layers. Positions are classified, never stripped -- the
17
+ * strippers delete a span with its contents, which destroys the digits. The
18
+ * prior art for classify-not-strip is `classifyHit`
19
+ * (`pr-closing-keywords/detect.ts`).
20
+ *
21
+ * The fence scan is local rather than borrowed from that detector because the
22
+ * detector's opener regex is anchored at column zero, so a CommonMark fence
23
+ * indented one to three spaces reads as prose. Relaxing it there would move
24
+ * hits into the #737 false-positive class and change what that gate suppresses,
25
+ * which is a separate decision from this one.
26
+ */
27
+ /** The published closed set, in the order the contract lists it. */
28
+ export const ACCEPTED_CITATION_FORMS = [
29
+ "successor lean 12345678",
30
+ "successor lean: 12345678",
31
+ "successor lean `12345678`",
32
+ "**successor lean:** 12345678",
33
+ "#issuecomment-12345678",
34
+ "/issues/comments/12345678",
35
+ ];
36
+ const EMPHASIS = "(?:\\*\\*|\\*)";
37
+ const KEYWORD = "(successor[ \\t]+lean|verified-claims[ \\t]+table|lean|comment)";
38
+ /**
39
+ * Colon and horizontal whitespace only. One of the three branches must consume
40
+ * a colon or a space, so `lean12345678` is not a citation.
41
+ */
42
+ const SEPARATOR = `(?::${EMPHASIS}?[ \\t]*|${EMPHASIS}:?[ \\t]+|[ \\t]+)`;
43
+ /** Bare decimal, or a balanced single-backtick decimal. Nothing else. */
44
+ const ID_TOKEN = "(?:`(\\d{8,})`|(\\d{8,})\\b)";
45
+ const CITATION_RE = new RegExp(`${EMPHASIS}?\\b${KEYWORD}${SEPARATOR}${ID_TOKEN}` +
46
+ "|#issuecomment-(\\d{8,})" +
47
+ "|/issues/comments/(\\d{8,})", "gi");
48
+ const ID_RUN_RE = /\d{8,}/g;
49
+ const BLOCKQUOTE_RE = /^ {0,3}>/;
50
+ /** CommonMark fence delimiter: up to three leading spaces, the run, then the rest. */
51
+ const FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
52
+ /** The last sentence break in a slice, and everything after it. */
53
+ const LAST_SENTENCE_RE = /[.!?;][^.!?;]*$/;
54
+ /** Everything up to the first sentence break. */
55
+ const FIRST_SENTENCE_RE = /^[^.!?;]*/;
56
+ /** The closed set of explicit negation markers the class recognises. */
57
+ const NEGATION_MARKER = "\\b(?:cannot|never|no longer" +
58
+ "|(?:do|does|did|is|are|was|were|has|have|had|will|would|shall|should|must|can|could|may|might)\\s+not" +
59
+ "|(?:do|does|did|is|are|was|were|has|have|had|wo|would|should|must|ca|could|sha)n['\\u2019]t)\\b";
60
+ /**
61
+ * Explicit negation of the citation itself, ending within three plain words of
62
+ * the keyword. A bare marker anywhere in the sentence prefix is not enough: it
63
+ * reads `without a doubt, successor lean N is accepted` and `not only successor
64
+ * lean N but also the table` as refusals, which blocks a valid record.
65
+ */
66
+ const NEGATED_CITATION_RE = new RegExp(`${NEGATION_MARKER}(?:\\s+[A-Za-z][A-Za-z'\\u2019-]*){0,3}\\s*$`, "i");
67
+ /** The same markers, unanchored, for the complement clause after a citation. */
68
+ const NEGATION_ANYWHERE_RE = new RegExp(NEGATION_MARKER, "i");
69
+ /**
70
+ * A negated verb of denial affirms the citation rather than refusing it, so the
71
+ * negation belongs to the verb and not to the occurrence: `we cannot deny that
72
+ * successor lean N binds` cites N. The verb set is closed and published --
73
+ * negating `deny`, `doubt`, `dispute`, `contest`, or `question` affirms the
74
+ * complement clause those verbs introduce.
75
+ *
76
+ * A trailing `that` alone is not the signal, because `that` is also a
77
+ * determiner. `do not use that successor lean N` and the cleft `the record is
78
+ * not that successor lean N` are genuine refusals, and both keep the negation
79
+ * class. The anchor is the end of the sentence segment, so a second negation
80
+ * before the keyword still binds: `we do not doubt that this does not bind
81
+ * successor lean N` is refused.
82
+ *
83
+ * The carve-out only suspends a prefix rule that already fired; it never
84
+ * refuses on its own, so it cannot widen the refused set. It also does not
85
+ * accept the citation outright. The complement clause is what carries the
86
+ * claim, so a negation anywhere in the rest of that sentence keeps the refusal:
87
+ * `we do not doubt that successor lean N does not bind` says the lean does not
88
+ * bind. That scan runs to the sentence break rather than to a clause boundary,
89
+ * which refuses more than it strictly must inside this one carve-out -- the
90
+ * fail-closed direction, and every such body was already refused before it.
91
+ */
92
+ const DENIAL_COMPLEMENT_RE = /\b(?:den(?:y|ies|ied|ying)|doubt(?:s|ed|ing)?|disput(?:e|es|ed|ing)|contest(?:s|ed|ing)?|question(?:s|ed|ing)?)\s+that\s*$/i;
93
+ function kindForKeyword(keyword) {
94
+ if (keyword === undefined)
95
+ return "comment";
96
+ const normalized = keyword.toLowerCase().replace(/[ \t]+/g, " ");
97
+ if (normalized.startsWith("verified-claims"))
98
+ return "table";
99
+ if (normalized === "comment")
100
+ return "comment";
101
+ return "lean";
102
+ }
103
+ function lineBounds(text, offset) {
104
+ const start = text.lastIndexOf("\n", offset - 1) + 1;
105
+ const found = text.indexOf("\n", offset);
106
+ return { start, end: found === -1 ? text.length : found };
107
+ }
108
+ /**
109
+ * CommonMark inline code: a run of N backticks opens a span that only a run of
110
+ * exactly N backticks closes. Counting single backticks would read a
111
+ * double-backtick span as balanced and let documentation prose cite.
112
+ *
113
+ * `text` is the whole span from the enclosing block start, not one line: a code
114
+ * span may carry a newline, so a line-scoped count cannot see the opener.
115
+ */
116
+ function isInsideInlineCode(text) {
117
+ let openRun = 0;
118
+ let i = 0;
119
+ while (i < text.length) {
120
+ if (text[i] !== "`") {
121
+ i += 1;
122
+ continue;
123
+ }
124
+ let run = 0;
125
+ while (i + run < text.length && text[i + run] === "`")
126
+ run += 1;
127
+ if (openRun === 0) {
128
+ openRun = run;
129
+ }
130
+ else if (run === openRun) {
131
+ openRun = 0;
132
+ }
133
+ i += run;
134
+ }
135
+ return openRun !== 0;
136
+ }
137
+ function isStruckThrough(text) {
138
+ let runs = 0;
139
+ let i = 0;
140
+ while (i < text.length - 1) {
141
+ if (text[i] === "~" && text[i + 1] === "~") {
142
+ runs += 1;
143
+ i += 2;
144
+ continue;
145
+ }
146
+ i += 1;
147
+ }
148
+ return runs % 2 === 1;
149
+ }
150
+ /**
151
+ * A negation binds the sentence it sits in, not the whole line. Scoping to the
152
+ * text after the last sentence break keeps `this is not stale. Bound contract
153
+ * is successor lean N` an affirmative citation.
154
+ */
155
+ function isNegated(line, column) {
156
+ const before = line.slice(0, column);
157
+ const tail = LAST_SENTENCE_RE.exec(before);
158
+ const segment = tail === null ? before : before.slice(tail.index + 1);
159
+ if (!NEGATED_CITATION_RE.test(segment))
160
+ return false;
161
+ if (!DENIAL_COMPLEMENT_RE.test(segment))
162
+ return true;
163
+ const after = line.slice(column);
164
+ return NEGATION_ANYWHERE_RE.test(FIRST_SENTENCE_RE.exec(after)?.[0] ?? after);
165
+ }
166
+ function fenceDelimiter(line) {
167
+ const match = FENCE_LINE_RE.exec(line);
168
+ if (match === null)
169
+ return null;
170
+ const run = match[1] ?? "";
171
+ const info = match[2] ?? "";
172
+ const char = run.startsWith("~") ? "~" : "`";
173
+ // A backtick opener's info string may not contain a backtick.
174
+ if (char === "`" && info.includes("`"))
175
+ return null;
176
+ return { char, len: run.length, info };
177
+ }
178
+ /**
179
+ * Walk the lines up to `offset` once and record both block facts the classifier
180
+ * needs. The fence stack follows CommonMark: opener and closer share the
181
+ * character, the closer is at least as long, and a closer carries no info
182
+ * string, so `` ```ts more text `` inside an open fence is content and not a
183
+ * closer. Inline scanning restarts after a blank line or a fence delimiter
184
+ * because neither a code span nor a strikethrough run crosses one. A blockquote
185
+ * marker carries forward to the blank line that ends the quote, so an unmarked
186
+ * lazy-continuation line is still quoted text.
187
+ *
188
+ * A fence delimiter also ends the quote block, and a line inside an open fence
189
+ * never opens one: a `>` in a fenced example is example text, so the marker must
190
+ * not survive the closing fence and refuse the citation that follows it. Lazy
191
+ * continuation is a paragraph rule, and a fence line is not paragraph text. A
192
+ * fence marked by `>` on every line is not a fence here -- `fenceDelimiter`
193
+ * requires the run at the line start -- so the line-level blockquote test still
194
+ * refuses a citation inside a blockquoted fence.
195
+ */
196
+ function blockPosition(body, offset) {
197
+ let open = null;
198
+ let inlineStart = 0;
199
+ let quotedBlock = false;
200
+ let lineStart = 0;
201
+ while (lineStart < offset) {
202
+ let lineEnd = body.indexOf("\n", lineStart);
203
+ const partial = lineEnd === -1 || lineEnd > offset;
204
+ if (partial) {
205
+ lineEnd = Math.min(body.length, offset);
206
+ }
207
+ const line = body.slice(lineStart, lineEnd);
208
+ const fence = fenceDelimiter(line);
209
+ if (fence !== null) {
210
+ if (open === null) {
211
+ open = fence;
212
+ }
213
+ else if (fence.char === open.char &&
214
+ fence.len >= open.len &&
215
+ fence.info.trim().length === 0) {
216
+ open = null;
217
+ }
218
+ }
219
+ if (!partial) {
220
+ const blank = line.trim().length === 0;
221
+ if (blank || fence !== null) {
222
+ inlineStart = lineEnd + 1;
223
+ quotedBlock = false;
224
+ }
225
+ else if (open === null && BLOCKQUOTE_RE.test(line)) {
226
+ quotedBlock = true;
227
+ }
228
+ }
229
+ if (lineEnd >= offset) {
230
+ break;
231
+ }
232
+ lineStart = lineEnd + 1;
233
+ }
234
+ return { insideFence: open !== null, inlineStart, quotedBlock };
235
+ }
236
+ /**
237
+ * Refused positions are exactly the five published classes. An indented code
238
+ * block and an HTML comment are deliberately not classified: a four-space
239
+ * indent is also ordinary list-continuation content, so refusing it would block
240
+ * valid records more often than it would catch example text.
241
+ */
242
+ function classifyPosition(body, offset) {
243
+ const block = blockPosition(body, offset);
244
+ if (block.insideFence)
245
+ return "code-fence";
246
+ const { start, end } = lineBounds(body, offset);
247
+ const line = body.slice(start, end);
248
+ if (BLOCKQUOTE_RE.test(line) || block.quotedBlock)
249
+ return "blockquote";
250
+ const preceding = body.slice(block.inlineStart, offset);
251
+ if (isInsideInlineCode(preceding))
252
+ return "inline-code";
253
+ if (isStruckThrough(preceding))
254
+ return "strikethrough";
255
+ if (isNegated(line, offset - start))
256
+ return "negation";
257
+ return null;
258
+ }
259
+ function idShapedRuns(body) {
260
+ const runs = [];
261
+ const seen = new Set();
262
+ const re = new RegExp(ID_RUN_RE.source, ID_RUN_RE.flags);
263
+ for (const match of body.matchAll(re)) {
264
+ const id = Number(match[0]);
265
+ if (!Number.isSafeInteger(id) || id <= 0 || seen.has(id))
266
+ continue;
267
+ seen.add(id);
268
+ runs.push(id);
269
+ }
270
+ return runs;
271
+ }
272
+ /**
273
+ * Parse the closed citation grammar out of one comment body. Refused positions
274
+ * are reported rather than dropped so a block detail can echo the observation
275
+ * instead of guessing at a cause.
276
+ */
277
+ export function scanCitations(body) {
278
+ const citations = [];
279
+ const rejected = [];
280
+ const seen = new Set();
281
+ const re = new RegExp(CITATION_RE.source, CITATION_RE.flags);
282
+ for (const match of body.matchAll(re)) {
283
+ const raw = match[2] ?? match[3] ?? match[4] ?? match[5];
284
+ if (raw === undefined)
285
+ continue;
286
+ const id = Number(raw);
287
+ if (!Number.isSafeInteger(id) || id <= 0)
288
+ continue;
289
+ const reason = classifyPosition(body, match.index ?? 0);
290
+ if (reason !== null) {
291
+ rejected.push({ id, reason });
292
+ continue;
293
+ }
294
+ const kind = kindForKeyword(match[1]);
295
+ const key = `${id}|${kind}`;
296
+ if (seen.has(key))
297
+ continue;
298
+ seen.add(key);
299
+ citations.push({ id, kind });
300
+ }
301
+ return { citations, rejected, idShapedRuns: idShapedRuns(body) };
302
+ }
303
+ //# sourceMappingURL=citation-grammar.js.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Completed-arc record for design-critique ingest (#3806).
3
+ *
4
+ * Catalog chips are list-visible convenience, not clearance. Ingest waits on
5
+ * `design-critique: synthesis accepted, because ...` citing an accepted
6
+ * successor lean (and the verified-claims table when that comment exists).
7
+ *
8
+ * Citations are parsed once, by `scanCitations` (#3831). The accepted forms and
9
+ * the refused positions are published in `content/contracts/design-critique.md`
10
+ * `## Citation grammar`. Clearance is set membership against the latest
11
+ * successor lean, not position in the body, so citing the superseded lean --
12
+ * which `## Successor lean` requires -- cannot block.
13
+ */
14
+ export type ThreadComment = {
15
+ readonly id: number;
16
+ readonly body: string;
17
+ };
18
+ export type CompletedArcBlockReason = "missing-record" | "lone-shape" | "cite-not-lean" | "missing-table-cite";
19
+ export type CompletedArcVerdict = {
20
+ readonly status: "not-in-arc";
21
+ } | {
22
+ readonly status: "complete";
23
+ readonly synthesisCommentId: number;
24
+ readonly citedLeanId: number;
25
+ readonly citedTableId: number | null;
26
+ } | {
27
+ readonly status: "blocked";
28
+ readonly reason: CompletedArcBlockReason;
29
+ readonly detail: string;
30
+ };
31
+ export declare class DesignCritiqueIngestBlockedError extends Error {
32
+ readonly issueNumber: number;
33
+ readonly reason: CompletedArcBlockReason;
34
+ constructor(issueNumber: number, reason: CompletedArcBlockReason, detail: string);
35
+ }
36
+ export declare function isSynthesisAcceptedShape(body: string): boolean;
37
+ export declare function isSuccessorLeanBody(body: string): boolean;
38
+ export declare function isVerifiedClaimsTableBody(body: string): boolean;
39
+ /**
40
+ * Ids cited by one comment body, in document order. Thin projection of the
41
+ * shared parser -- the grammar itself lives in `citation-grammar.ts`.
42
+ */
43
+ export declare function extractCitedCommentIds(body: string): number[];
44
+ export declare function hasDesignCritiqueCatalogChip(labels: readonly string[]): boolean;
45
+ export declare function isPanelDepositBody(body: string): boolean;
46
+ export declare function isInFlightCritiqueThread(comments: readonly ThreadComment[]): boolean;
47
+ /**
48
+ * Ingest clearance from thread structure. Labels and author identity are not
49
+ * predicates. A lone synthesis-accepted sentence shape is not the record.
50
+ * Clearance cites the latest successor lean; an older complete record does not
51
+ * clear a later recut. A panel-deposit is in-flight even before critic posts.
52
+ */
53
+ export declare function evaluateCompletedArcRecord(input: {
54
+ readonly labels?: readonly string[];
55
+ readonly comments: readonly ThreadComment[];
56
+ }): CompletedArcVerdict;
57
+ export declare function assertCompletedArcAllowsIngest(input: {
58
+ readonly issueNumber: number;
59
+ readonly labels?: readonly string[];
60
+ readonly comments: readonly ThreadComment[];
61
+ }): CompletedArcVerdict;
62
+ //# sourceMappingURL=completed-arc-record.d.ts.map