@deftai/directive-core 0.102.0 → 0.103.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.
@@ -42,7 +42,13 @@ export declare function installerManagedMatchers(): InstallerManagedMatcher[];
42
42
  * so an unlisted `xbrief/active/another-scope.xbrief.json` matcher still fails.
43
43
  */
44
44
  export declare function assertInstallerAllowlistHonors1430(matchers?: readonly InstallerManagedMatcher[]): void;
45
- /** POSIX ERE alternation embedded in the deposited deft-core-guard workflow. */
45
+ /**
46
+ * Individual POSIX ERE patterns for the deposited deft-core-guard allowlist.
47
+ * One pattern per line in the workflow heredoc (#3345) — joined form is still
48
+ * available via {@link installerManagedGuardEre} for tests and classifiers.
49
+ */
50
+ export declare function installerManagedGuardErePatterns(): string[];
51
+ /** POSIX ERE alternation (joined) for tests / tooling that want a single pattern. */
46
52
  export declare function installerManagedGuardEre(): string;
47
53
  export declare function isInstallerManagedPath(path: string): boolean;
48
54
  export interface MixedCoreAndAppClassification {
@@ -174,12 +174,20 @@ export function assertInstallerAllowlistHonors1430(matchers = installerManagedMa
174
174
  }
175
175
  }
176
176
  }
177
- /** POSIX ERE alternation embedded in the deposited deft-core-guard workflow. */
178
- export function installerManagedGuardEre() {
177
+ /**
178
+ * Individual POSIX ERE patterns for the deposited deft-core-guard allowlist.
179
+ * One pattern per line in the workflow heredoc (#3345) — joined form is still
180
+ * available via {@link installerManagedGuardEre} for tests and classifiers.
181
+ */
182
+ export function installerManagedGuardErePatterns() {
179
183
  const matchers = installerManagedMatchers();
180
184
  // Refuse to emit a guard workflow that would exempt consumer denylist paths.
181
185
  assertInstallerAllowlistHonors1430(matchers);
182
- return matchers.map((matcher) => matcherToEre(matcher)).join("|");
186
+ return matchers.map((matcher) => matcherToEre(matcher));
187
+ }
188
+ /** POSIX ERE alternation (joined) for tests / tooling that want a single pattern. */
189
+ export function installerManagedGuardEre() {
190
+ return installerManagedGuardErePatterns().join("|");
183
191
  }
184
192
  export function isInstallerManagedPath(path) {
185
193
  return matchesInstallerManaged(path, installerManagedMatchers());
@@ -82,6 +82,18 @@ export declare function coreGuardCheckoutUsesLine(sha?: string, tag?: string): s
82
82
  * the managed guard script / allowlist body (#1672 option 1).
83
83
  */
84
84
  export declare function mergeCoreGuardWorkflowRefresh(existing: string, desired: string): string;
85
+ /**
86
+ * Soft max line length for deposited core-guard workflow YAML (#3345).
87
+ * Mega-line allowlist EREs previously sat near ~5k chars; keep well below.
88
+ * Enforced by {@link assertCoreGuardWorkflowLoadable} before deposit write.
89
+ */
90
+ export declare const CORE_GUARD_WORKFLOW_MAX_LINE = 500;
91
+ /**
92
+ * Fail closed if a rendered deft-core-guard workflow would not load in GHA
93
+ * (#3345): any `run: |` content line less indented than the block base, or a
94
+ * line longer than {@link CORE_GUARD_WORKFLOW_MAX_LINE}.
95
+ */
96
+ export declare function assertCoreGuardWorkflowLoadable(content: string): void;
85
97
  export declare function ensureGitattributes(projectDir: string, io: InitDepositIo): boolean;
86
98
  export declare function ensureGreptileIgnore(projectDir: string, io: InitDepositIo): boolean;
87
99
  export declare function ensureCodeqlPathsIgnore(projectDir: string, io: InitDepositIo): boolean;
@@ -14,7 +14,7 @@ import { assertDestinationNotSymlink, ProjectionContainmentError, } from "../fs/
14
14
  import { agentsRefreshPlan } from "../platform/agents-md.js";
15
15
  import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
16
16
  import { CANONICAL_INSTALL_ROOT } from "./constants.js";
17
- import { assertInstallerAllowlistHonors1430, installerManagedGuardEre } from "./hygiene.js";
17
+ import { assertInstallerAllowlistHonors1430, installerManagedGuardErePatterns } from "./hygiene.js";
18
18
  import { writeAgentsSkillsFromInventory } from "./skill-discovery-deposit.js";
19
19
  import { syncConsumerXbriefSchemas } from "./xbrief-projections.js";
20
20
  export { CANONICAL_INSTALL_ROOT };
@@ -501,16 +501,66 @@ export function mergeCoreGuardWorkflowRefresh(existing, desired) {
501
501
  return desired;
502
502
  return desired.slice(0, idx) + existingCheckout + desired.slice(idx + desiredCheckout.length);
503
503
  }
504
+ /**
505
+ * Indent for shell lines inside the deposited workflow `run: |` block.
506
+ * YAML literal blocks end at any line less indented than this; the #3193
507
+ * Python body must stay at this column so GHA can load the workflow (#3345).
508
+ */
509
+ const CORE_GUARD_RUN_INDENT = " ";
510
+ /**
511
+ * Soft max line length for deposited core-guard workflow YAML (#3345).
512
+ * Mega-line allowlist EREs previously sat near ~5k chars; keep well below.
513
+ * Enforced by {@link assertCoreGuardWorkflowLoadable} before deposit write.
514
+ */
515
+ export const CORE_GUARD_WORKFLOW_MAX_LINE = 500;
516
+ /**
517
+ * Fail closed if a rendered deft-core-guard workflow would not load in GHA
518
+ * (#3345): any `run: |` content line less indented than the block base, or a
519
+ * line longer than {@link CORE_GUARD_WORKFLOW_MAX_LINE}.
520
+ */
521
+ export function assertCoreGuardWorkflowLoadable(content) {
522
+ if (!content.startsWith("name: deft-core-guard\n") &&
523
+ !content.startsWith("name: deft-core-guard\r\n")) {
524
+ throw new Error("deft-core-guard workflow must start with name: deft-core-guard (#3345)");
525
+ }
526
+ let inRun = false;
527
+ let baseIndent = null;
528
+ for (const [index, line] of content.split("\n").entries()) {
529
+ if (line.length > CORE_GUARD_WORKFLOW_MAX_LINE) {
530
+ throw new Error(`deft-core-guard workflow line ${index + 1} is ${line.length} chars ` +
531
+ `(max ${CORE_GUARD_WORKFLOW_MAX_LINE}; #3345 loadable YAML)`);
532
+ }
533
+ if (line.trim() === "run: |") {
534
+ inRun = true;
535
+ baseIndent = null;
536
+ continue;
537
+ }
538
+ if (!inRun || line.trim() === "")
539
+ continue;
540
+ const lead = line.match(/^( *)/)?.[1]?.length ?? 0;
541
+ if (baseIndent === null) {
542
+ baseIndent = lead;
543
+ continue;
544
+ }
545
+ if (lead < baseIndent) {
546
+ throw new Error(`deft-core-guard workflow line ${index + 1} dedents below run-block base ` +
547
+ `(indent ${lead} < ${baseIndent}; #3345 YAML load failure class)`);
548
+ }
549
+ }
550
+ }
504
551
  /**
505
552
  * Embedded python3 content check for package.json / lockfiles when co-travelling
506
553
  * with .deft/core/** (#3193). Mirrors TS `isUpgradePinPathContentAllowed`.
507
554
  * Uses a single-quoted heredoc so GHA does not interpolate shell variables
508
555
  * inside the Python source (BASE/HEAD are argv).
556
+ *
557
+ * Every emitted line is indented to {@link CORE_GUARD_RUN_INDENT} so the YAML
558
+ * `run: |` block stays intact (#3345). After GHA strips the common indent, the
559
+ * shell heredoc body reaches python3 with correct relative indentation.
509
560
  */
510
561
  function coreGuardPinContentPython() {
511
562
  // Keep this compact: deposited into every consumer workflow on init/update.
512
- return [
513
- ' python3 - "$BASE_SHA" "$HEAD_SHA" <<\'PY\'',
563
+ const pyBody = [
514
564
  "import json, re, subprocess, sys",
515
565
  "base_sha, head_sha = sys.argv[1], sys.argv[2]",
516
566
  "def git_show(sha, path):",
@@ -766,7 +816,31 @@ function coreGuardPinContentPython() {
766
816
  " print('\\n'.join(bad))",
767
817
  " sys.exit(1)",
768
818
  "print('OK: package/lock content is Directive pin unit (#3193).')",
769
- "PY",
819
+ ];
820
+ const run = CORE_GUARD_RUN_INDENT;
821
+ return [
822
+ `${run}python3 - "$BASE_SHA" "$HEAD_SHA" <<'PY'`,
823
+ ...pyBody.map((line) => `${run}${line}`),
824
+ `${run}PY`,
825
+ ].join("\n");
826
+ }
827
+ /**
828
+ * Shell that materializes the installer-managed allowlist as one ERE per line
829
+ * and filters "app" paths with `grep -vE -f` (#3345 — avoids a ~5k-char line
830
+ * that contributed to invalid workflow load). Semantics match the prior
831
+ * single-alternation ERE (OR of all matchers).
832
+ */
833
+ function coreGuardAllowlistShell() {
834
+ const run = CORE_GUARD_RUN_INDENT;
835
+ const patterns = installerManagedGuardErePatterns();
836
+ return [
837
+ `${run}# Installer-managed allowlist — one ERE per line (#3345 loadable YAML)`,
838
+ `${run}allowlist=$(mktemp)`,
839
+ `${run}trap 'rm -f "$allowlist"' EXIT`,
840
+ `${run}cat > "$allowlist" <<'ALLOW'`,
841
+ ...patterns.map((p) => `${run}${p}`),
842
+ `${run}ALLOW`,
843
+ `${run}app=$(printf '%s\\n' "$changed" | grep -vE '^\\.deft/core/' | grep -vE -f "$allowlist" | grep -v '^$' || true)`,
770
844
  ].join("\n");
771
845
  }
772
846
  function coreGuardWorkflowContent() {
@@ -775,7 +849,7 @@ function coreGuardWorkflowContent() {
775
849
  const baseSha = githubActionsExpr("github.event.pull_request.base.sha");
776
850
  const headSha = githubActionsExpr("github.event.pull_request.head.sha");
777
851
  return ("name: deft-core-guard\n\n" +
778
- "# Deft framework guard (#1430 / #3127 / #3193): a single PR should not mix changes to the\n" +
852
+ "# Deft framework guard (#1430 / #3127 / #3193 / #3345): a single PR should not mix changes to the\n" +
779
853
  "# vendored framework payload (.deft/core/**) with true application/product files.\n" +
780
854
  "# One upgrade PR MAY include deposit + installer-managed paths + package.json pin/lock\n" +
781
855
  "# + .deft/GENERATION.json when package.json is @deftai/directive* dependency-key pin-only\n" +
@@ -803,9 +877,7 @@ function coreGuardWorkflowContent() {
803
877
  ' echo "Changed files:"\n' +
804
878
  ' echo "$changed"\n' +
805
879
  " core=$(printf '%s\\n' \"$changed\" | grep -E '^\\.deft/core/' || true)\n" +
806
- " app=$(printf '%s\\n' \"$changed\" | grep -vE '^\\.deft/core/' | grep -vE '" +
807
- installerManagedGuardEre() +
808
- "' | grep -v '^$' || true)\n" +
880
+ `${coreGuardAllowlistShell()}\n` +
809
881
  ' if [ -n "$core" ] && [ -n "$app" ]; then\n' +
810
882
  ' echo "::error title=deft-core guard (#1430)::This PR changes the vendored framework payload (.deft/core/**) AND non-framework files. Split the framework update into its own PR."\n' +
811
883
  ' echo "--- framework (.deft/core/**) changes ---"; printf \'%s\\n\' "$core"\n' +
@@ -951,6 +1023,7 @@ export function ensureCodeqlPathsIgnore(projectDir, io) {
951
1023
  export function ensureCoreGuardWorkflow(projectDir, io) {
952
1024
  const path = projectionTarget(projectDir, CORE_GUARD_WORKFLOW_REL);
953
1025
  const desired = coreGuardWorkflowContent();
1026
+ assertCoreGuardWorkflowLoadable(desired);
954
1027
  if (existsSync(path)) {
955
1028
  const existing = readFileSync(path, "utf8");
956
1029
  if (!existing.includes("name: deft-core-guard")) {
@@ -62,6 +62,12 @@ export interface EvaluateVerifyAcOptions extends EvaluateLiteralAcceptanceOption
62
62
  * emit after the #3285 bank checkpoint.
63
63
  */
64
64
  readonly skipAcceptanceEmit?: boolean;
65
+ /**
66
+ * Active scope key for product-oracle check_id namespacing (#3337).
67
+ * Prefer plan.id; path stem when id is missing. Multi-active verify:ac
68
+ * under one session must not share a single global `verify:ac` check id.
69
+ */
70
+ readonly oracleScopeKey?: string | null;
65
71
  }
66
72
  /**
67
73
  * Evaluate product AC from an in-memory plan.
@@ -71,6 +77,12 @@ export declare function evaluateVerifyAcFromPlan(plan: Record<string, unknown>,
71
77
  * Evaluate from xBRIEF path.
72
78
  */
73
79
  export declare function evaluateVerifyAcFromPath(xbriefPath: string, options?: EvaluateVerifyAcOptions): VerifyAcResult;
80
+ /**
81
+ * Unique product-oracle scope key for one active xBRIEF path (#3337).
82
+ * Relative path is always unique across active roots; plan.id alone is not
83
+ * (duplicate ids / same stem in xbrief+vbrief). Prefer `id@relPath` when both exist.
84
+ */
85
+ export declare function resolveOracleScopeKey(plan: Record<string, unknown>, xbriefPath: string, projectRoot: string): string;
74
86
  /** Pure: product AC is required at every ceremony depth (#3284 / #3267 / #3156). */
75
87
  export declare function isVerifyAcRequiredAtCeremonyDepth(_depth: string | null | undefined): boolean;
76
88
  //# sourceMappingURL=evaluate.d.ts.map
@@ -8,7 +8,7 @@
8
8
  * floor, empty resolution is soft_empty — not a green run (#3334).
9
9
  */
10
10
  import { existsSync, readFileSync } from "node:fs";
11
- import { basename, resolve } from "node:path";
11
+ import { basename, isAbsolute, relative, resolve } from "node:path";
12
12
  import { evaluateLiteralAcceptanceFromPlan, runLiteralAcceptanceCommands, } from "../literal-acceptance/index.js";
13
13
  import { ENV_RUN_SUMMARY_PATH, RunSummaryEmitter, } from "../run-summary/index.js";
14
14
  import { maybeBankOnAcPass } from "../session/ac-pass-banking.js";
@@ -26,6 +26,11 @@ function asRecord(value) {
26
26
  * Evaluate product AC from an in-memory plan.
27
27
  */
28
28
  export function evaluateVerifyAcFromPlan(plan, options = {}) {
29
+ const planId = typeof plan.id === "string" && plan.id.trim() ? plan.id.trim() : null;
30
+ const optionsWithScope = {
31
+ ...options,
32
+ oracleScopeKey: options.oracleScopeKey?.trim() || planId || null,
33
+ };
29
34
  const acceptance = readPlanAcceptance(plan);
30
35
  const schemaErrors = validatePlanAcceptance(plan.acceptance ?? acceptance);
31
36
  // Only hard-fail schema when an explicit plan.acceptance object exists.
@@ -41,9 +46,9 @@ export function evaluateVerifyAcFromPlan(plan, options = {}) {
41
46
  acceptance,
42
47
  resolution: "config",
43
48
  resolvedCommandCount: 0,
44
- }, options);
49
+ }, optionsWithScope);
45
50
  }
46
- const projectRoot = resolve(options.projectRoot ?? process.cwd());
51
+ const projectRoot = resolve(optionsWithScope.projectRoot ?? process.cwd());
47
52
  // Prefer shared literal-acceptance path so safety / promotion rules stay one place.
48
53
  // Empty plan.acceptance.commands still consults the #3267 rejected ledger
49
54
  // (Greptile P1: rejected stated AC must never soft-pass).
@@ -51,12 +56,13 @@ export function evaluateVerifyAcFromPlan(plan, options = {}) {
51
56
  // if the literal ledger is empty of executables.
52
57
  const base = evaluateLiteralAcceptanceFromPlan(plan, {
53
58
  projectRoot,
54
- runner: options.runner,
59
+ runner: optionsWithScope.runner,
55
60
  // Check composition uses the stamped ledger only. Re-scanning issue prose
56
61
  // during `task check` re-captures backtick `verify:ac` lines as rejected
57
62
  // and deadlocks the graph (#3323 / #3284 check-integrated).
58
- captureFromNarratives: options.captureFromNarratives ?? (options.checkIntegrated === true ? false : undefined),
59
- quiet: options.quiet,
63
+ captureFromNarratives: optionsWithScope.captureFromNarratives ??
64
+ (optionsWithScope.checkIntegrated === true ? false : undefined),
65
+ quiet: optionsWithScope.quiet,
60
66
  });
61
67
  // When literal path had nothing executable but plan.acceptance has derived commands,
62
68
  // run them directly with source=explicit semantics.
@@ -64,7 +70,7 @@ export function evaluateVerifyAcFromPlan(plan, options = {}) {
64
70
  base.runs.length === 0 &&
65
71
  acceptance.commands.length > 0 &&
66
72
  (acceptance.source_rung === "derived" || acceptance.source_rung === "project_floor")) {
67
- const runner = options.runner;
73
+ const runner = optionsWithScope.runner;
68
74
  const direct = runLiteralAcceptanceCommands(acceptance.commands.map((c) => ({
69
75
  command: c.command,
70
76
  cwd: c.cwd ?? null,
@@ -72,17 +78,21 @@ export function evaluateVerifyAcFromPlan(plan, options = {}) {
72
78
  expectedExitCode: c.expectedExitCode ?? 0,
73
79
  source: "explicit",
74
80
  sourceSpan: "plan.acceptance.commands",
75
- })), { projectRoot, runner, allowTaskStatement: options.allowTaskStatement });
76
- return applyOracle(annotate(direct, acceptance, options.quiet), options);
81
+ })), {
82
+ projectRoot,
83
+ runner,
84
+ allowTaskStatement: optionsWithScope.allowTaskStatement,
85
+ });
86
+ return applyOracle(annotate(direct, acceptance, optionsWithScope.quiet), optionsWithScope);
77
87
  }
78
88
  // Check composition: mid-story unpromoted capture-only may soft-pass so the
79
89
  // framework graph is not deadlocked before agents promote peers.
80
90
  // Greptile P1 #3284: safety-rejected stated commands NEVER soft-pass — they
81
91
  // block product verification until a safe alternative is promoted.
82
- if (options.checkIntegrated === true && !base.ok && base.runs.length === 0) {
92
+ if (optionsWithScope.checkIntegrated === true && !base.ok && base.runs.length === 0) {
83
93
  const hasRejected = (base.rejected?.length ?? 0) > 0;
84
94
  if (hasRejected) {
85
- return applyOracle(annotate(base, acceptance, options.quiet), options);
95
+ return applyOracle(annotate(base, acceptance, optionsWithScope.quiet), optionsWithScope);
86
96
  }
87
97
  const unpromoted = /capture-only|task_statement|no matching agent-promoted/i.test(base.message) ||
88
98
  (base.commands.length > 0 && base.commands.every((c) => c.source === "task_statement"));
@@ -90,7 +100,7 @@ export function evaluateVerifyAcFromPlan(plan, options = {}) {
90
100
  return applyOracle({
91
101
  ok: true,
92
102
  code: 0,
93
- message: options.quiet
103
+ message: optionsWithScope.quiet
94
104
  ? ""
95
105
  : `verify:ac advisory (#3284 check-integrated): no executable AC peers yet ` +
96
106
  `(capture-only / empty). Done-gate standalone verify:ac still requires promotion. ` +
@@ -110,10 +120,10 @@ export function evaluateVerifyAcFromPlan(plan, options = {}) {
110
120
  rejectedCount: base.rejected?.length ?? 0,
111
121
  }),
112
122
  resolvedCommandCount: base.commands.length,
113
- }, options);
123
+ }, optionsWithScope);
114
124
  }
115
125
  }
116
- return applyOracle(annotate(base, acceptance, options.quiet), options);
126
+ return applyOracle(annotate(base, acceptance, optionsWithScope.quiet), optionsWithScope);
117
127
  }
118
128
  function classifyResolution(input) {
119
129
  if (input.resolution !== undefined) {
@@ -242,6 +252,7 @@ function applyOracle(result, options) {
242
252
  projectRoot,
243
253
  runs: gated.runs,
244
254
  env: options.env,
255
+ scopeKey: options.oracleScopeKey,
245
256
  });
246
257
  }
247
258
  let next = gated;
@@ -324,11 +335,36 @@ export function evaluateVerifyAcFromPath(xbriefPath, options = {}) {
324
335
  if (plan === null) {
325
336
  return applyOracle(configResult(`verify:ac: xBRIEF missing plan object: ${abs}`), options);
326
337
  }
327
- const result = evaluateVerifyAcFromPlan(plan, { ...options, skipAcceptanceEmit: true });
338
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
339
+ // Path-relative keys stay unique across xbrief/ vs vbrief/ and duplicate plan.id (#3337 Greptile).
340
+ const oracleScopeKey = options.oracleScopeKey?.trim() || resolveOracleScopeKey(plan, abs, projectRoot);
341
+ const result = evaluateVerifyAcFromPlan(plan, {
342
+ ...options,
343
+ skipAcceptanceEmit: true,
344
+ oracleScopeKey,
345
+ });
328
346
  const banked = maybeAttachAcPassBank(result, plan, abs, options);
329
- emitAcceptanceOutcome(banked, options, resolve(options.projectRoot ?? process.cwd()));
347
+ emitAcceptanceOutcome(banked, options, projectRoot);
330
348
  return banked;
331
349
  }
350
+ /**
351
+ * Unique product-oracle scope key for one active xBRIEF path (#3337).
352
+ * Relative path is always unique across active roots; plan.id alone is not
353
+ * (duplicate ids / same stem in xbrief+vbrief). Prefer `id@relPath` when both exist.
354
+ */
355
+ export function resolveOracleScopeKey(plan, xbriefPath, projectRoot) {
356
+ const abs = resolve(xbriefPath);
357
+ const root = resolve(projectRoot);
358
+ let rel = relative(root, abs).replace(/\\/g, "/");
359
+ if (rel.length === 0 || rel.startsWith("..") || isAbsolute(rel)) {
360
+ rel = basename(abs);
361
+ }
362
+ const planId = typeof plan.id === "string" && plan.id.trim() ? plan.id.trim() : null;
363
+ if (planId !== null) {
364
+ return `${planId}@${rel}`;
365
+ }
366
+ return rel;
367
+ }
332
368
  /**
333
369
  * After executable AC pass, FINALIZE the banking checkpoint (#3285).
334
370
  * Soft/advisory passes with zero runs do not bank. Fail-open on ledger errors.
@@ -7,6 +7,6 @@
7
7
  export { attachPlanAcceptance, buildAcceptanceFromIntakeCapture, readPlanAcceptance, stampAcceptanceFromLiteralCapture, validatePlanAcceptance, } from "./acceptance.js";
8
8
  export { applyProductFirstGateMode, isHygieneGate, isProductAcGate, type ProductFirstCheckModeResolution, type ResolveProductFirstCheckModeInput, resolveProductFirstCheckMode, } from "./check-mode.js";
9
9
  export { EMPTY_AC_CAUSE, EMPTY_AC_OUTCOME, EMPTY_AC_REMEDY, formatSoftEmptyMessage, isEmptyAcResolution, isSoftEmptyAcText, projectHasSuiteFloor, type VerifyAcResolution, } from "./empty-resolution.js";
10
- export { type EvaluateVerifyAcOptions, evaluateVerifyAcFromPath, evaluateVerifyAcFromPlan, isVerifyAcRequiredAtCeremonyDepth, type VerifyAcResult, } from "./evaluate.js";
10
+ export { type EvaluateVerifyAcOptions, evaluateVerifyAcFromPath, evaluateVerifyAcFromPlan, isVerifyAcRequiredAtCeremonyDepth, resolveOracleScopeKey, type VerifyAcResult, } from "./evaluate.js";
11
11
  export { type AcceptanceCommand, type AcSourceRung, ENV_CHECK_AC_ONLY, ENV_CHECK_MODE, ENV_HYGIENE_ADVISORY, HYGIENE_GATE_ID_PREFIXES, PLAN_ACCEPTANCE_KEY, type PlanAcceptance, PRODUCT_AC_GATE_ID, type ProductFirstCheckMode, } from "./types.js";
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -7,6 +7,6 @@
7
7
  export { attachPlanAcceptance, buildAcceptanceFromIntakeCapture, readPlanAcceptance, stampAcceptanceFromLiteralCapture, validatePlanAcceptance, } from "./acceptance.js";
8
8
  export { applyProductFirstGateMode, isHygieneGate, isProductAcGate, resolveProductFirstCheckMode, } from "./check-mode.js";
9
9
  export { EMPTY_AC_CAUSE, EMPTY_AC_OUTCOME, EMPTY_AC_REMEDY, formatSoftEmptyMessage, isEmptyAcResolution, isSoftEmptyAcText, projectHasSuiteFloor, } from "./empty-resolution.js";
10
- export { evaluateVerifyAcFromPath, evaluateVerifyAcFromPlan, isVerifyAcRequiredAtCeremonyDepth, } from "./evaluate.js";
10
+ export { evaluateVerifyAcFromPath, evaluateVerifyAcFromPlan, isVerifyAcRequiredAtCeremonyDepth, resolveOracleScopeKey, } from "./evaluate.js";
11
11
  export { ENV_CHECK_AC_ONLY, ENV_CHECK_MODE, ENV_HYGIENE_ADVISORY, HYGIENE_GATE_ID_PREFIXES, PLAN_ACCEPTANCE_KEY, PRODUCT_AC_GATE_ID, } from "./types.js";
12
12
  //# sourceMappingURL=index.js.map
@@ -27,6 +27,16 @@ export interface OracleIntegrityResultFields {
27
27
  readonly code: number;
28
28
  readonly message: string;
29
29
  }
30
+ /** Stable prefix for product-oracle check ids emitted by verify:ac (#3322 / #3337). */
31
+ export declare const VERIFY_AC_CHECK_ID_PREFIX = "verify:ac";
32
+ /**
33
+ * Namespace product-oracle check_id per active scope (#3337).
34
+ *
35
+ * Same-session multi-active verify:ac must not pair fail/pass across briefs.
36
+ * Prefer plan.id or xBRIEF path as scopeKey; empty → global fallback `verify:ac`
37
+ * (unknown-scope single-brief / tests without a plan).
38
+ */
39
+ export declare function verifyAcCheckId(scopeKey?: string | null): string;
30
40
  /** Test seam: isolate stdout-dest buffer across cases. */
31
41
  export declare function resetInProcessVerificationBuffer(): void;
32
42
  /**
@@ -34,12 +44,20 @@ export declare function resetInProcessVerificationBuffer(): void;
34
44
  * Fail-open: missing dest / write errors never change the AC result.
35
45
  * Stdout dest also appends to the same-process buffer so evaluate can
36
46
  * inspect this invocation without re-reading a file.
47
+ * check_id is namespaced per active scope when scopeKey is provided (#3337).
37
48
  */
38
49
  export declare function emitVerifyAcAttempts(options: {
39
50
  readonly projectRoot: string;
40
51
  readonly runs: readonly LiteralAcceptanceRunResult[];
41
52
  readonly env?: NodeJS.ProcessEnv;
42
53
  readonly sessionId?: string;
54
+ /**
55
+ * Active scope identity (plan.id or xBRIEF path stem). When set, check_id
56
+ * becomes `verify:ac/<scopeKey>` so multi-active sessions do not false-deny (#3337).
57
+ */
58
+ readonly scopeKey?: string | null;
59
+ /** Full check_id override (tests). Wins over scopeKey when non-empty. */
60
+ readonly checkId?: string | null;
43
61
  /** stdout seam (tests). */
44
62
  readonly writeStdout?: (line: string) => void;
45
63
  }): void;
@@ -16,7 +16,24 @@ function formatUnresolved(flag) {
16
16
  return (`check_id=${flag.check_id} fail method=${flag.failed_method} ` +
17
17
  `then pass method=${flag.passed_method} without independent re-derivation`);
18
18
  }
19
- const VERIFY_AC_CHECK_ID = "verify:ac";
19
+ /** Stable prefix for product-oracle check ids emitted by verify:ac (#3322 / #3337). */
20
+ export const VERIFY_AC_CHECK_ID_PREFIX = "verify:ac";
21
+ /**
22
+ * Namespace product-oracle check_id per active scope (#3337).
23
+ *
24
+ * Same-session multi-active verify:ac must not pair fail/pass across briefs.
25
+ * Prefer plan.id or xBRIEF path as scopeKey; empty → global fallback `verify:ac`
26
+ * (unknown-scope single-brief / tests without a plan).
27
+ */
28
+ export function verifyAcCheckId(scopeKey) {
29
+ const key = typeof scopeKey === "string" ? scopeKey.trim() : "";
30
+ if (key.length === 0) {
31
+ return VERIFY_AC_CHECK_ID_PREFIX;
32
+ }
33
+ // Keep pairing keys readable; strip control chars that would corrupt JSONL.
34
+ const safe = key.replace(/[\0\n\r]/g, "_");
35
+ return `${VERIFY_AC_CHECK_ID_PREFIX}/${safe}`;
36
+ }
20
37
  /** Same-process verification JSONL when dest is stdout (`-`). */
21
38
  const inProcessVerificationLines = [];
22
39
  let inProcessStdoutSessionId;
@@ -36,6 +53,7 @@ function inProcessVerificationText() {
36
53
  * Fail-open: missing dest / write errors never change the AC result.
37
54
  * Stdout dest also appends to the same-process buffer so evaluate can
38
55
  * inspect this invocation without re-reading a file.
56
+ * check_id is namespaced per active scope when scopeKey is provided (#3337).
39
57
  */
40
58
  export function emitVerifyAcAttempts(options) {
41
59
  if (options.runs.length === 0) {
@@ -57,6 +75,10 @@ export function emitVerifyAcAttempts(options) {
57
75
  sessionId = randomUUID();
58
76
  }
59
77
  }
78
+ const explicitCheck = typeof options.checkId === "string" && options.checkId.trim().length > 0
79
+ ? options.checkId.trim()
80
+ : null;
81
+ const checkId = explicitCheck ?? verifyAcCheckId(options.scopeKey);
60
82
  const emitter = new RunSummaryEmitter({
61
83
  projectRoot,
62
84
  sessionId,
@@ -65,7 +87,7 @@ export function emitVerifyAcAttempts(options) {
65
87
  });
66
88
  for (const run of options.runs) {
67
89
  const emitted = emitter.emitVerification({
68
- check_id: VERIFY_AC_CHECK_ID,
90
+ check_id: checkId,
69
91
  method_fingerprint: `${run.command}\0${run.cwd}`,
70
92
  outcome: run.ok ? "pass" : "fail",
71
93
  });
@@ -5,6 +5,6 @@
5
5
  * by editing the comparison. Deterministic surface is the flagged event.
6
6
  */
7
7
  export { type AcceptanceClause, type AcceptanceClauseReading, type ClauseOutcome, type ClauseWalkReport, type ClauseWalkResult, deriveAcceptanceClauses, formatClauseWalkMessage, isScratchArtifactPath, readAcceptanceClauses, serializeAcceptanceClauses, stampDerivedClausesOnAcceptance, walkAcceptanceClauses, } from "./clauses.js";
8
- export { type EvaluateProductOracleIntegrityOptions, emitVerifyAcAttempts, evaluateProductOracleIntegrity, mergeOracleVerdict, type OracleIntegrityResultFields, type ProductOracleIntegrityVerdict, } from "./evaluate.js";
8
+ export { type EvaluateProductOracleIntegrityOptions, emitVerifyAcAttempts, evaluateProductOracleIntegrity, mergeOracleVerdict, type OracleIntegrityResultFields, type ProductOracleIntegrityVerdict, VERIFY_AC_CHECK_ID_PREFIX, verifyAcCheckId, } from "./evaluate.js";
9
9
  export { type FlaggedMethodChangePass, flagPassAfterFailFromJsonl, flagPassAfterFailWithMethodChange, readVerificationAttempts, unresolvedMethodChangePasses, type VerificationAttempt, } from "./flag.js";
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -5,6 +5,6 @@
5
5
  * by editing the comparison. Deterministic surface is the flagged event.
6
6
  */
7
7
  export { deriveAcceptanceClauses, formatClauseWalkMessage, isScratchArtifactPath, readAcceptanceClauses, serializeAcceptanceClauses, stampDerivedClausesOnAcceptance, walkAcceptanceClauses, } from "./clauses.js";
8
- export { emitVerifyAcAttempts, evaluateProductOracleIntegrity, mergeOracleVerdict, } from "./evaluate.js";
8
+ export { emitVerifyAcAttempts, evaluateProductOracleIntegrity, mergeOracleVerdict, VERIFY_AC_CHECK_ID_PREFIX, verifyAcCheckId, } from "./evaluate.js";
9
9
  export { flagPassAfterFailFromJsonl, flagPassAfterFailWithMethodChange, readVerificationAttempts, unresolvedMethodChangePasses, } from "./flag.js";
10
10
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.102.0",
3
+ "version": "0.103.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -382,8 +382,8 @@
382
382
  "provenance": true
383
383
  },
384
384
  "dependencies": {
385
- "@deftai/directive-content": "^0.102.0",
386
- "@deftai/directive-types": "^0.102.0",
385
+ "@deftai/directive-content": "^0.103.0",
386
+ "@deftai/directive-types": "^0.103.0",
387
387
  "archiver": "^8.0.0"
388
388
  },
389
389
  "scripts": {