@deftai/directive-core 0.97.0 → 0.98.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 (47) hide show
  1. package/dist/authz/classify.js +291 -0
  2. package/dist/check/cached-orchestrator.d.ts +5 -0
  3. package/dist/check/cached-orchestrator.js +18 -1
  4. package/dist/check/gate-lists.d.ts +20 -0
  5. package/dist/check/gate-lists.js +46 -9
  6. package/dist/check/index.d.ts +1 -1
  7. package/dist/check/index.js +1 -1
  8. package/dist/check/orchestrator.d.ts +4 -0
  9. package/dist/check/orchestrator.js +4 -0
  10. package/dist/doctor/checks.d.ts +7 -0
  11. package/dist/doctor/checks.js +83 -0
  12. package/dist/hooks/dispatcher.d.ts +5 -0
  13. package/dist/hooks/dispatcher.js +54 -4
  14. package/dist/init-deposit/hygiene.d.ts +70 -1
  15. package/dist/init-deposit/hygiene.js +582 -8
  16. package/dist/init-deposit/scaffold.js +277 -4
  17. package/dist/init-deposit/skill-discovery-deposit.js +15 -0
  18. package/dist/policy/check-resume.d.ts +72 -0
  19. package/dist/policy/check-resume.js +253 -0
  20. package/dist/policy/coverage-check-resume-presets.d.ts +46 -0
  21. package/dist/policy/coverage-check-resume-presets.js +228 -0
  22. package/dist/policy/coverage-debt.d.ts +76 -0
  23. package/dist/policy/coverage-debt.js +262 -0
  24. package/dist/policy/index.d.ts +3 -0
  25. package/dist/policy/index.js +50 -21
  26. package/dist/release/auto-hatch.d.ts +114 -0
  27. package/dist/release/auto-hatch.js +301 -0
  28. package/dist/release/coverage-debt-ledger.d.ts +22 -0
  29. package/dist/release/coverage-debt-ledger.js +157 -0
  30. package/dist/release/index.d.ts +3 -0
  31. package/dist/release/index.js +3 -0
  32. package/dist/release/pipeline.js +164 -12
  33. package/dist/release/suite-stamp.d.ts +44 -0
  34. package/dist/release/suite-stamp.js +133 -0
  35. package/dist/release/types.d.ts +19 -0
  36. package/dist/session/coverage-check-resume-nudge.d.ts +34 -0
  37. package/dist/session/coverage-check-resume-nudge.js +66 -0
  38. package/dist/session/index.d.ts +1 -0
  39. package/dist/session/index.js +1 -0
  40. package/dist/session/session-start.js +21 -0
  41. package/dist/triage/classify/label-mirror.d.ts +31 -1
  42. package/dist/triage/classify/label-mirror.js +78 -6
  43. package/dist/triage/help/registry-data.d.ts +6 -6
  44. package/dist/triage/help/registry-data.js +12 -3
  45. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  46. package/dist/vbrief-validate/plan-hooks.js +54 -0
  47. package/package.json +3 -3
@@ -3,6 +3,9 @@ import { CANONICAL_GITIGNORE_BASELINE } from "../init-deposit/gitignore.js";
3
3
  import { detectDualLayout, detectLegacyLayout, dualLayoutSignpostLine, legacyLayoutSignpostLine, } from "../init-deposit/legacy-detect.js";
4
4
  import { detectCanonicalVendoredManifest, isNpmManaged, NPM_MANAGED_SENTINEL_KEY, NPM_MANAGED_SENTINEL_VALUE, } from "../init-deposit/migrate.js";
5
5
  import { resolveLifecycleRoot } from "../layout/resolve.js";
6
+ import { resolveCheckResume } from "../policy/check-resume.js";
7
+ import { resolveCoverageDebt } from "../policy/coverage-debt.js";
8
+ import { policyColonInvocation } from "../policy/policy-invocation.js";
6
9
  import { findSkillPathsInText } from "../text/redos-safe.js";
7
10
  import { stripGitignoreInlineComment } from "../triage/bootstrap/gitignore.js";
8
11
  import { LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
@@ -725,6 +728,83 @@ export function checkGitignoreCoverage(projectRoot, seams = {}) {
725
728
  },
726
729
  };
727
730
  }
731
+ /**
732
+ * Surface undecided / invalid coverageDebt + checkResume policy (#3189).
733
+ * Advisory skip when undecided; never hard-fails doctor / check:consumer.
734
+ * Decided-off is quiet; dismiss-with-reason is pass with reason in detail.
735
+ * Invalid typed blocks resolve fail-closed and surface via source=default-on-error.
736
+ */
737
+ export function checkCoverageCheckResumePolicy(projectRoot) {
738
+ const debt = resolveCoverageDebt(projectRoot);
739
+ const resume = resolveCheckResume(projectRoot);
740
+ if (debt.source === "default-on-error" || resume.source === "default-on-error") {
741
+ return {
742
+ name: "coverage-check-resume-policy",
743
+ status: "skip",
744
+ detail: "advisory: coverageDebt and/or checkResume block is invalid; " +
745
+ "resolution is fail-closed (mode off, localStamp off, CI never trusts stamps). " +
746
+ `coverageDebt.error=${JSON.stringify(debt.error)}; ` +
747
+ `checkResume.error=${JSON.stringify(resume.error)}. ` +
748
+ "Fix the typed block or re-apply Strict / Hatch-aware / dismiss-with-reason.",
749
+ data: {
750
+ coverageDebt: { status: debt.status, source: debt.source, error: debt.error },
751
+ checkResume: { status: resume.status, source: resume.source, error: resume.error },
752
+ advisory: true,
753
+ invalid: true,
754
+ },
755
+ };
756
+ }
757
+ const undecided = debt.status === "unset" || resume.status === "unset";
758
+ if (!undecided) {
759
+ const dismissParts = [];
760
+ if (debt.dismissReason) {
761
+ dismissParts.push(`coverageDebt.dismissReason=${JSON.stringify(debt.dismissReason)}`);
762
+ }
763
+ if (resume.dismissReason) {
764
+ dismissParts.push(`checkResume.dismissReason=${JSON.stringify(resume.dismissReason)}`);
765
+ }
766
+ const dismissNote = dismissParts.length > 0 ? ` Dismissed: ${dismissParts.join("; ")}.` : "";
767
+ return {
768
+ name: "coverage-check-resume-policy",
769
+ status: "pass",
770
+ detail: `coverageDebt status=${debt.status} mode=${debt.mode}; ` +
771
+ `checkResume status=${resume.status} localStamp=${resume.localStamp}; ` +
772
+ `ciTrustsLocalStamp=false (fixed v1).${dismissNote}`,
773
+ data: {
774
+ coverageDebt: {
775
+ status: debt.status,
776
+ mode: debt.mode,
777
+ autoFile: debt.autoFile,
778
+ dismissReason: debt.dismissReason,
779
+ },
780
+ checkResume: {
781
+ status: resume.status,
782
+ localStamp: resume.localStamp,
783
+ ciTrustsLocalStamp: false,
784
+ dismissReason: resume.dismissReason,
785
+ },
786
+ },
787
+ };
788
+ }
789
+ // Advisory only (status=skip): never hard-fails doctor / check:consumer (#3189).
790
+ // Behavior remains fail-closed while unset; session-start nudge carries the ask.
791
+ return {
792
+ name: "coverage-check-resume-policy",
793
+ status: "skip",
794
+ detail: "advisory: coverageDebt and/or checkResume policy is undecided (status=unset). " +
795
+ "Behavior stays fail-closed (no hatch soft-pass, no local suite stamp, CI never trusts stamps). " +
796
+ "Choose Strict / Hatch-aware on the next interactive session-start nudge, or record " +
797
+ "dismiss-with-reason. Inspect: " +
798
+ `\`${policyColonInvocation("show", " --field=coverageDebt")}\` / ` +
799
+ `\`${policyColonInvocation("show", " --field=checkResume")}\`.`,
800
+ data: {
801
+ coverageDebt: { status: debt.status, mode: debt.mode },
802
+ checkResume: { status: resume.status, localStamp: resume.localStamp },
803
+ suggested_fix: policyColonInvocation("show", " --field=coverageDebt"),
804
+ advisory: true,
805
+ },
806
+ };
807
+ }
728
808
  export function deriveExitCode(checks, errors) {
729
809
  const exitExempt = new Set([
730
810
  "canonical-vendored-npm-signpost",
@@ -732,6 +812,7 @@ export function deriveExitCode(checks, errors) {
732
812
  "gitignore-coverage",
733
813
  "stale-xbrief-schema-deposit",
734
814
  "typescript-7-side-by-side",
815
+ "coverage-check-resume-policy",
735
816
  ]);
736
817
  if (errors.length > 0 || checks.some((c) => c.status === "error")) {
737
818
  return 2;
@@ -774,6 +855,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
774
855
  checks.push(checkStaleXbriefSchemaDeposit(projectRoot, seams));
775
856
  checks.push(checkGitignoreCoverage(projectRoot, seams));
776
857
  checks.push(checkTypescript7SideBySide(projectRoot, seams));
858
+ checks.push(checkCoverageCheckResumePolicy(projectRoot));
777
859
  return {
778
860
  projectRoot,
779
861
  installRoot: null,
@@ -792,6 +874,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
792
874
  checks.push(checkStaleXbriefSchemaDeposit(projectRoot, seams));
793
875
  checks.push(checkGitignoreCoverage(projectRoot, seams));
794
876
  checks.push(checkTypescript7SideBySide(projectRoot, seams));
877
+ checks.push(checkCoverageCheckResumePolicy(projectRoot));
795
878
  return {
796
879
  projectRoot,
797
880
  installRoot,
@@ -119,6 +119,11 @@ export declare const ASSIST_SCRATCH_ROOT_PREFIXES: readonly [".deft-scratch/", "
119
119
  * True when the write target is under an allowlisted disposable scratch root (#1802).
120
120
  * Fail closed on null/empty/unparseable targets and on path escape (`..`).
121
121
  * Does not authorize tracked product paths even under assist posture.
122
+ *
123
+ * #3186: after lexical allowlist match, realpath the scratch root and require
124
+ * {@link assertProjectionContained}; refuse symlink scratch roots and any
125
+ * realpath outside the project (host Write would follow the link).
126
+ * When the project root cannot be realpath'd (unit fixtures), lexical classification wins.
122
127
  */
123
128
  export declare function isAllowlistedAssistScratchPath(projectRoot: string, targetPath: string | null): boolean;
124
129
  /**
@@ -1,6 +1,7 @@
1
- import { realpathSync } from "node:fs";
1
+ import { lstatSync, realpathSync } from "node:fs";
2
2
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
3
  import { appendAuthzAudit, classifyHookAuthzOps, evaluateAuthzMutation, evidenceSatisfiesImplementationApproval, listActiveHumanGrants, loadAuthzStateResult, markGrantUsed, shouldConsumeSingleUseGrant, utcIso, } from "../authz/index.js";
4
+ import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
4
5
  import { hasArtifactSuffix } from "../layout/resolve.js";
5
6
  import { detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
6
7
  import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
@@ -110,6 +111,11 @@ export const ASSIST_SCRATCH_ROOT_PREFIXES = [".deft-scratch/", "temp/"];
110
111
  * True when the write target is under an allowlisted disposable scratch root (#1802).
111
112
  * Fail closed on null/empty/unparseable targets and on path escape (`..`).
112
113
  * Does not authorize tracked product paths even under assist posture.
114
+ *
115
+ * #3186: after lexical allowlist match, realpath the scratch root and require
116
+ * {@link assertProjectionContained}; refuse symlink scratch roots and any
117
+ * realpath outside the project (host Write would follow the link).
118
+ * When the project root cannot be realpath'd (unit fixtures), lexical classification wins.
113
119
  */
114
120
  export function isAllowlistedAssistScratchPath(projectRoot, targetPath) {
115
121
  if (targetPath === null || targetPath.trim().length === 0)
@@ -120,11 +126,55 @@ export function isAllowlistedAssistScratchPath(projectRoot, targetPath) {
120
126
  return false;
121
127
  if (isLexicalOutsideProjectRoot(posix))
122
128
  return false;
129
+ let matchedPrefix = null;
123
130
  for (const prefix of ASSIST_SCRATCH_ROOT_PREFIXES) {
124
- if (posix === prefix.slice(0, -1) || posix.startsWith(prefix))
125
- return true;
131
+ if (posix === prefix.slice(0, -1) || posix.startsWith(prefix)) {
132
+ matchedPrefix = prefix;
133
+ break;
134
+ }
126
135
  }
127
- return false;
136
+ if (matchedPrefix === null)
137
+ return false;
138
+ // #3186 containment: realpath scratch root + projection fence (symlink escape).
139
+ const projectAbs = resolve(projectRoot);
140
+ try {
141
+ realpathSync(projectAbs);
142
+ }
143
+ catch {
144
+ // Unit fixtures / missing project dir — lexical allowlist only.
145
+ return true;
146
+ }
147
+ const scratchRootName = matchedPrefix.slice(0, -1); // ".deft-scratch" | "temp"
148
+ const scratchRootAbs = resolve(projectAbs, scratchRootName);
149
+ try {
150
+ // Refuse when the scratch root (or any parent on the path) escapes via symlink.
151
+ assertProjectionContained(projectAbs, scratchRootAbs);
152
+ }
153
+ catch (err) {
154
+ if (err instanceof ProjectionContainmentError)
155
+ return false;
156
+ return false;
157
+ }
158
+ // Refuse symlink scratch roots entirely (#3186) — even in-tree links can divert Write.
159
+ try {
160
+ const st = lstatSync(scratchRootAbs);
161
+ if (st.isSymbolicLink())
162
+ return false;
163
+ }
164
+ catch {
165
+ // Scratch root does not exist yet — mkdir will create a real directory; allow.
166
+ }
167
+ // Also fence the concrete write target when it already exists on disk.
168
+ const targetAbs = resolve(projectAbs, targetPath.replace(/\\/g, "/"));
169
+ try {
170
+ assertProjectionContained(projectAbs, targetAbs);
171
+ }
172
+ catch (err) {
173
+ if (err instanceof ProjectionContainmentError)
174
+ return false;
175
+ return false;
176
+ }
177
+ return true;
128
178
  }
129
179
  /**
130
180
  * Assist/ephemeral classification for scratch-write carve-out (#1802).
@@ -8,7 +8,7 @@
8
8
  * and scope briefs are never installer-managed; if they reappear in
9
9
  * `installerManagedMatchers()`, unit tests and deposit-time assert fail closed.
10
10
  *
11
- * Refs #1576, #1453, #1430, #3029, #3030, #3127, #3117.
11
+ * Refs #1576, #1453, #1430, #3029, #3030, #3127, #3117, #3193.
12
12
  */
13
13
  import { type InitDepositIo } from "./constants.js";
14
14
  export declare const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
@@ -51,13 +51,82 @@ export interface MixedCoreAndAppClassification {
51
51
  readonly app: string[];
52
52
  /** True when both core and app are non-empty — the deposited guard fails. */
53
53
  readonly wouldFail: boolean;
54
+ /**
55
+ * Pin/lock paths that were path-allowlisted but failed content-aware checks
56
+ * when co-travelling with core (#3193). Empty when path-only classification.
57
+ */
58
+ readonly pinContentRejected?: string[];
59
+ }
60
+ /** Paths that path-allowlist for upgrade co-travel but need content checks with core (#3193). */
61
+ export declare const UPGRADE_PIN_CONTENT_PATHS: readonly ["package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
62
+ export type UpgradePinContentPath = (typeof UPGRADE_PIN_CONTENT_PATHS)[number];
63
+ /** package.json dependency map fields that may hold @deftai/directive* pins. */
64
+ export declare const PACKAGE_JSON_DEP_FIELDS: readonly ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
65
+ /**
66
+ * True when `name` is an `@deftai/directive*` dependency **key** (#3193).
67
+ * Substring hits in scripts/settings values do not qualify.
68
+ */
69
+ export declare function isDirectiveDependencyKey(name: string): boolean;
70
+ /**
71
+ * package.json co-travel is allowed only when the sole differences are
72
+ * `@deftai/directive*` dependency-key pins under the standard dep maps (#3193).
73
+ * Scripts/settings/metadata that merely contain the substring still fail.
74
+ */
75
+ export declare function isPackageJsonDirectivePinOnlyDiff(baseRaw: string, headRaw: string): boolean;
76
+ /**
77
+ * package-lock.json follow-through: non-@deftai/directive* root direct dependency
78
+ * identities (and their `node_modules/<name>` package entries when present) must
79
+ * be unchanged. Directive pin identity + transitive/resolution churn may change.
80
+ */
81
+ export declare function isPackageLockDirectivePinFollowThrough(baseRaw: string, headRaw: string): boolean;
82
+ /**
83
+ * Minimal pnpm-lock.yaml (v6/v9) root importer (`.`) direct-dep extractor.
84
+ * Avoids a YAML dependency; sufficient for pin follow-through identity checks.
85
+ */
86
+ export declare function pnpmLockRootDirectDeps(raw: string): Record<string, string>;
87
+ /**
88
+ * Extract pnpm `packages:` section records keyed by the package name (not
89
+ * name@version). Used to freeze product package resolution blocks.
90
+ */
91
+ export declare function pnpmPackagesByName(raw: string): Map<string, string>;
92
+ export declare function isPnpmLockDirectivePinFollowThrough(baseRaw: string, headRaw: string): boolean;
93
+ /**
94
+ * yarn.lock (v1) package identity blocks: every non-@deftai/directive* package
95
+ * must keep identical block text (or fail if added/removed). Directive package
96
+ * blocks may change freely as pin follow-through (#3193).
97
+ */
98
+ export declare function isYarnLockDirectivePinFollowThrough(baseRaw: string, headRaw: string): boolean;
99
+ /**
100
+ * Content-aware check for a single upgrade pin path (#3193).
101
+ * Returns true when the path may co-travel with `.deft/core/**`.
102
+ */
103
+ export declare function isUpgradePinPathContentAllowed(path: string, baseRaw: string, headRaw: string): boolean;
104
+ export interface PinContentFilePair {
105
+ readonly base: string;
106
+ readonly head: string;
54
107
  }
55
108
  /**
56
109
  * TS twin of Go `classifyChangedPaths` / deposited shell guard (#1430).
57
110
  * Core = `.deft/core/**`; installer-managed = allowlist; app = everything else.
58
111
  * Guard fails iff both core and app are non-empty.
112
+ *
113
+ * Path-only: does not inspect package/lock contents. Prefer
114
+ * {@link classifyMixedCoreAndAppContentAware} when base/head blobs are available
115
+ * (deposited guard + unit tests for #3193).
59
116
  */
60
117
  export declare function classifyMixedCoreAndApp(changedPaths: readonly string[], matchers?: readonly InstallerManagedMatcher[]): MixedCoreAndAppClassification;
118
+ /**
119
+ * Content-aware upgrade co-travel classifier (#3193).
120
+ *
121
+ * Starts from path classification (#3127 allowlist), then when `.deft/core/**`
122
+ * is present reclassifies package.json / lockfile paths as **app** unless their
123
+ * base→head content is the Directive pin unit (or lock follow-through).
124
+ * `.deft/GENERATION.json` remains path-allowlisted with no content constraint.
125
+ *
126
+ * Missing content for a pin path co-travelling with core fails closed (treated
127
+ * as app) so partial fixtures cannot silently re-open the path-only hole.
128
+ */
129
+ export declare function classifyMixedCoreAndAppContentAware(changedPaths: readonly string[], fileContents: Readonly<Partial<Record<string, PinContentFilePair>>>, matchers?: readonly InstallerManagedMatcher[]): MixedCoreAndAppClassification;
61
130
  export interface FrameworkStagePathsOptions {
62
131
  /**
63
132
  * Include the vendored `.deft/core` payload in the stage set. Defaults to