@deftai/directive-core 0.99.0 → 0.100.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.
- package/dist/authz/actions.d.ts +6 -0
- package/dist/authz/actions.js +3 -0
- package/dist/authz/classify.js +157 -23
- package/dist/authz/decompose-apply.d.ts +67 -0
- package/dist/authz/decompose-apply.js +302 -0
- package/dist/authz/index.d.ts +3 -1
- package/dist/authz/index.js +3 -1
- package/dist/authz/store.d.ts +53 -0
- package/dist/authz/store.js +219 -1
- package/dist/authz/types.d.ts +19 -2
- package/dist/authz/types.js +5 -1
- package/dist/doctor/checks.d.ts +44 -0
- package/dist/doctor/checks.js +428 -2
- package/dist/doctor/main.d.ts +6 -7
- package/dist/doctor/main.js +72 -79
- package/dist/doctor/which.d.ts +20 -1
- package/dist/doctor/which.js +67 -1
- package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
- package/dist/finish-loop/pr-finish-loop.js +116 -5
- package/dist/finish-loop/types.d.ts +1 -1
- package/dist/hooks/dispatcher.d.ts +9 -5
- package/dist/hooks/dispatcher.js +23 -15
- package/dist/hooks/readonly.d.ts +6 -3
- package/dist/hooks/readonly.js +44 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +20 -0
- package/dist/init-deposit/agent-hooks.js +51 -31
- package/dist/intake/issue-ingest.js +29 -0
- package/dist/lifecycle/completed-consistency.d.ts +60 -0
- package/dist/lifecycle/completed-consistency.js +443 -0
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/literal-acceptance/capture.d.ts +52 -0
- package/dist/literal-acceptance/capture.js +658 -0
- package/dist/literal-acceptance/evaluate.d.ts +47 -0
- package/dist/literal-acceptance/evaluate.js +193 -0
- package/dist/literal-acceptance/index.d.ts +12 -0
- package/dist/literal-acceptance/index.js +12 -0
- package/dist/literal-acceptance/run.d.ts +38 -0
- package/dist/literal-acceptance/run.js +216 -0
- package/dist/literal-acceptance/safety.d.ts +24 -0
- package/dist/literal-acceptance/safety.js +251 -0
- package/dist/literal-acceptance/types.d.ts +82 -0
- package/dist/literal-acceptance/types.js +26 -0
- package/dist/policy/ceremony-dial.d.ts +67 -11
- package/dist/policy/ceremony-dial.js +174 -23
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/policy/merge-approval-head.d.ts +129 -0
- package/dist/policy/merge-approval-head.js +461 -0
- package/dist/policy/require-human-merge.d.ts +5 -0
- package/dist/policy/require-human-merge.js +5 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
- package/dist/pr-merge-readiness/ci-gate.js +65 -5
- package/dist/pr-merge-readiness/compute.d.ts +13 -0
- package/dist/pr-merge-readiness/compute.js +99 -14
- package/dist/pr-merge-readiness/gh.d.ts +49 -0
- package/dist/pr-merge-readiness/gh.js +268 -0
- package/dist/pr-merge-readiness/index.d.ts +2 -2
- package/dist/pr-merge-readiness/index.js +2 -2
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
- package/dist/pr-wait-mergeable/cascade.js +115 -1
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +4 -0
- package/dist/pr-wait-mergeable/types.d.ts +3 -1
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
- package/dist/pr-wait-mergeable/wrappers.js +7 -1
- package/dist/preflight/evaluate.d.ts +15 -1
- package/dist/preflight/evaluate.js +33 -2
- package/dist/render/framework-commands.js +4 -0
- package/dist/scope/acceptance-evidence.d.ts +76 -0
- package/dist/scope/acceptance-evidence.js +348 -0
- package/dist/scope/coverage-map.d.ts +98 -0
- package/dist/scope/coverage-map.js +558 -0
- package/dist/scope/decompose.js +125 -11
- package/dist/scope/index.d.ts +3 -0
- package/dist/scope/index.js +3 -0
- package/dist/scope/parent-lineage.d.ts +125 -0
- package/dist/scope/parent-lineage.js +626 -0
- package/dist/scope/transition.d.ts +8 -0
- package/dist/scope/transition.js +61 -2
- package/dist/session/active-cli.d.ts +79 -0
- package/dist/session/active-cli.js +382 -0
- package/dist/session/effort-budget.d.ts +130 -0
- package/dist/session/effort-budget.js +334 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/session-start.d.ts +6 -0
- package/dist/session/session-start.js +53 -2
- package/dist/session/verify-session-ritual.d.ts +14 -0
- package/dist/session/verify-session-ritual.js +33 -0
- package/dist/story-ready/evaluate.d.ts +11 -0
- package/dist/story-ready/evaluate.js +41 -3
- package/dist/triage/actions/candidates-log.d.ts +1 -2
- package/dist/triage/actions/candidates-log.js +37 -6
- package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
- package/dist/xbrief-migrate/migrate-project.js +92 -3
- package/package.json +7 -3
package/dist/authz/types.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* agent authors must not independently satisfy implementation-approval gates.
|
|
6
6
|
* Local-file forgery by a credential-compromised agent remains #983-class OOS.
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
8
|
+
/** Structural apply op for scope:decompose (#3239 / epic #3237 Q2). Always gated. */
|
|
9
|
+
export const SCOPE_DECOMPOSE_APPLY_STRUCTURAL = "scope.decompose.apply.structural";
|
|
10
|
+
/** Operation classes bound on a grant (Wave 1 structural binding + Wave 4 closed verbs + #3239). */
|
|
9
11
|
export const AUTHZ_OPERATIONS = [
|
|
10
12
|
"edit",
|
|
11
13
|
"push",
|
|
@@ -18,6 +20,8 @@ export const AUTHZ_OPERATIONS = [
|
|
|
18
20
|
"release-cut",
|
|
19
21
|
"release-publish",
|
|
20
22
|
"release-rollback",
|
|
23
|
+
/** Human-origin exact-draft-digest gate for scope:decompose apply (#3239). */
|
|
24
|
+
SCOPE_DECOMPOSE_APPLY_STRUCTURAL,
|
|
21
25
|
];
|
|
22
26
|
/**
|
|
23
27
|
* Human-origin kinds accepted as approval provenance.
|
package/dist/doctor/checks.d.ts
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
|
+
import type { XbriefSchemaDistance } from "../staleness-tickler/types.js";
|
|
1
2
|
import type { CheckResult } from "./types.js";
|
|
3
|
+
/** Remediation verb for project envelope behind-major (#2971 / #3243 / #3236). */
|
|
4
|
+
export declare const XBRIEF_ENVELOPE_MIGRATE_COMMAND: "deft migrate:xbrief";
|
|
5
|
+
/** Doctor check name for envelope major mismatch (Q5 Option 2, #3243). */
|
|
6
|
+
export declare const XBRIEF_ENVELOPE_MAJOR_CHECK: "xbrief-envelope-version";
|
|
2
7
|
export interface CheckSeams {
|
|
3
8
|
readonly readText?: (path: string) => string | null;
|
|
4
9
|
readonly isFile?: (path: string) => boolean;
|
|
5
10
|
readonly isDir?: (path: string) => boolean;
|
|
11
|
+
/** List directory entries; throws on enum failure (fail-closed for live lifecycle dirs). */
|
|
12
|
+
readonly readdir?: (path: string) => string[];
|
|
6
13
|
}
|
|
14
|
+
/** One scanned project envelope and its schema distance vs the framework target. */
|
|
15
|
+
export interface XbriefEnvelopeScanEntry {
|
|
16
|
+
readonly relativePath: string;
|
|
17
|
+
readonly declaredVersion: string | null;
|
|
18
|
+
readonly distance: XbriefSchemaDistance;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Scan live project xbrief envelopes (PROJECT-DEFINITION + pending/active) for
|
|
22
|
+
* schema distance vs the installed framework envelope major (#3243). Complements
|
|
23
|
+
* #2971 PROJECT-DEFINITION-only probe; remediation aligns with migrate hybrid
|
|
24
|
+
* rewrite (#3236).
|
|
25
|
+
*/
|
|
26
|
+
export declare function scanXbriefEnvelopeVersions(projectRoot: string, seams?: CheckSeams, targetVersion?: string): {
|
|
27
|
+
readonly targetVersion: string;
|
|
28
|
+
readonly entries: readonly XbriefEnvelopeScanEntry[];
|
|
29
|
+
readonly worstDistance: XbriefSchemaDistance | null;
|
|
30
|
+
readonly behindMajor: readonly XbriefEnvelopeScanEntry[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Fail closed when any scanned project xBRIEF envelope declares a major behind
|
|
34
|
+
* the framework target (#3243 / epic #3237 Q5 Option 2). Behind-minor does not
|
|
35
|
+
* fail this check. Remediation: `deft migrate:xbrief` (rewrites hybrid 0.6 on
|
|
36
|
+
* already-xbrief trees — #3236). Distinct from `stale-xbrief-schema-deposit`
|
|
37
|
+
* which routes schema-only drift to `directive update`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function checkXbriefEnvelopeMajorVersion(projectRoot: string, seams?: CheckSeams): CheckResult;
|
|
7
40
|
export declare function checkQuickStartResolves(projectRoot: string, installRoot: string | null, seams?: CheckSeams): CheckResult;
|
|
8
41
|
export declare function checkSkillPathsResolve(projectRoot: string, agentsMdText: string, seams?: CheckSeams): CheckResult;
|
|
9
42
|
export declare function checkManifestAgreement(projectRoot: string, installRoot: string | null, seams?: CheckSeams): CheckResult;
|
|
@@ -60,6 +93,17 @@ export declare function checkGitignoreCoverage(projectRoot: string, seams?: Chec
|
|
|
60
93
|
* Decided-off is quiet; dismiss-with-reason is pass with reason in detail.
|
|
61
94
|
* Invalid typed blocks resolve fail-closed and surface via source=default-on-error.
|
|
62
95
|
*/
|
|
96
|
+
/**
|
|
97
|
+
* Fail closed when completed/ plan.status disagrees with the folder (#3242 AC1 / epic #3237 Q4).
|
|
98
|
+
* Open plan.items under completed/ are reported by `checkCompletedOpenItems` (exit-exempt
|
|
99
|
+
* until historical corpora are backfilled; scope:complete enforces items hard).
|
|
100
|
+
*/
|
|
101
|
+
export declare function checkCompletedLifecycleConsistency(projectRoot: string): CheckResult;
|
|
102
|
+
/**
|
|
103
|
+
* Surface non-terminal plan.items under completed/ (#3242). Exit-exempt so
|
|
104
|
+
* historical pre-#2862 corpora do not red-light doctor; scope:complete fails closed.
|
|
105
|
+
*/
|
|
106
|
+
export declare function checkCompletedOpenItems(projectRoot: string): CheckResult;
|
|
63
107
|
export declare function checkCoverageCheckResumePolicy(projectRoot: string): CheckResult;
|
|
64
108
|
export declare function deriveExitCode(checks: readonly CheckResult[], errors: readonly string[]): number;
|
|
65
109
|
export declare function runChecksImpl(projectRoot: string, seams?: CheckSeams & {
|
package/dist/doctor/checks.js
CHANGED
|
@@ -1,21 +1,363 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { VBRIEF_VERSION } from "@deftai/directive-types";
|
|
2
4
|
import { CANONICAL_GITIGNORE_BASELINE } from "../init-deposit/gitignore.js";
|
|
3
5
|
import { detectDualLayout, detectLegacyLayout, dualLayoutSignpostLine, legacyLayoutSignpostLine, } from "../init-deposit/legacy-detect.js";
|
|
4
6
|
import { detectCanonicalVendoredManifest, isNpmManaged, NPM_MANAGED_SENTINEL_KEY, NPM_MANAGED_SENTINEL_VALUE, } from "../init-deposit/migrate.js";
|
|
5
7
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
8
|
+
import { scanCompletedLifecycleConsistency } from "../lifecycle/completed-consistency.js";
|
|
6
9
|
import { resolveCheckResume } from "../policy/check-resume.js";
|
|
7
10
|
import { resolveCoverageDebt } from "../policy/coverage-debt.js";
|
|
8
11
|
import { policyColonInvocation } from "../policy/policy-invocation.js";
|
|
12
|
+
import { classifyXbriefSchemaDistance } from "../staleness-tickler/probe-xbrief.js";
|
|
9
13
|
import { findSkillPathsInText } from "../text/redos-safe.js";
|
|
10
14
|
import { stripGitignoreInlineComment } from "../triage/bootstrap/gitignore.js";
|
|
11
|
-
import { LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
|
|
15
|
+
import { LEGACY_ARTIFACT_DIR, LEGACY_INFO_ROOT_KEY, LEGACY_VBRIEF_VERSION, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, } from "../xbrief-migrate/constants.js";
|
|
12
16
|
import { detectXbriefConvergence } from "../xbrief-migrate/detect.js";
|
|
17
|
+
import { readDeclaredArtifactVersion } from "../xbrief-migrate/transforms.js";
|
|
13
18
|
import { CANONICAL_UPGRADE_COMMAND, GO_BRIDGE_RELEASES_URL, UPGRADING_DOC_URL, } from "./constants.js";
|
|
14
19
|
import { isDeprecationRedirectStub, locateManifest, manifestCandidatePaths, manifestReportableVersion, manifestTagToVersion, parseInstallManifest, parseInstallRootFromAgentsMd, parseManifest, } from "./manifest.js";
|
|
15
20
|
import { readTextSafe } from "./paths.js";
|
|
21
|
+
/** Remediation verb for project envelope behind-major (#2971 / #3243 / #3236). */
|
|
22
|
+
export const XBRIEF_ENVELOPE_MIGRATE_COMMAND = "deft migrate:xbrief";
|
|
23
|
+
/** Doctor check name for envelope major mismatch (Q5 Option 2, #3243). */
|
|
24
|
+
export const XBRIEF_ENVELOPE_MAJOR_CHECK = "xbrief-envelope-version";
|
|
25
|
+
/** True when an fs error means the path is cleanly absent (not unreadable). */
|
|
26
|
+
function isEnoentError(err) {
|
|
27
|
+
return (typeof err === "object" &&
|
|
28
|
+
err !== null &&
|
|
29
|
+
"code" in err &&
|
|
30
|
+
err.code === "ENOENT");
|
|
31
|
+
}
|
|
16
32
|
function readText(path, seams) {
|
|
17
33
|
return (seams.readText ?? readTextSafe)(path);
|
|
18
34
|
}
|
|
35
|
+
function isDirectoryPath(path, seams) {
|
|
36
|
+
if (seams.isDir) {
|
|
37
|
+
return seams.isDir(path);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return statSync(path).isDirectory();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Probe whether `path` is a file, absent, or unstatable (include for fail-closed). */
|
|
47
|
+
function probeFilePresence(path, seams) {
|
|
48
|
+
if (seams.isFile) {
|
|
49
|
+
try {
|
|
50
|
+
return seams.isFile(path) ? "present" : "absent";
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
return isEnoentError(err) ? "absent" : "unreadable";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return statSync(path).isFile() ? "present" : "absent";
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// ENOENT → cleanly missing; EACCES/EPERM/other → treat as live but unreadable
|
|
61
|
+
// so Doctor cannot skip/pass past a definition it cannot inspect (#3243).
|
|
62
|
+
return isEnoentError(err) ? "absent" : "unreadable";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Probe whether `path` is a directory, absent, or unstatable.
|
|
67
|
+
* Unreadable pending/active must fail closed via enumFailures, not silent skip.
|
|
68
|
+
*/
|
|
69
|
+
function probeDirectoryPresence(path, seams) {
|
|
70
|
+
if (seams.isDir) {
|
|
71
|
+
try {
|
|
72
|
+
return seams.isDir(path) ? "present" : "absent";
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return isEnoentError(err) ? "absent" : "unreadable";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return statSync(path).isDirectory() ? "present" : "absent";
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return isEnoentError(err) ? "absent" : "unreadable";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Live project envelopes for fail-closed major mismatch (#3243 first ship).
|
|
87
|
+
* PROJECT-DEFINITION + pending/active in-flight work. Proposed backlog and
|
|
88
|
+
* completed/cancelled archives are not fail-closed here (historical debt; full
|
|
89
|
+
* tree rewrite remains on `deft migrate:xbrief` / #3236).
|
|
90
|
+
*/
|
|
91
|
+
const ENVELOPE_MAJOR_SCAN_FOLDERS = ["pending", "active"];
|
|
92
|
+
/**
|
|
93
|
+
* Collect live-path `*.xbrief.json` envelopes under xbrief/ for major check.
|
|
94
|
+
* Existing lifecycle dirs that cannot be listed are returned as
|
|
95
|
+
* `enumFailures` (directory relative paths) so the scan fails closed rather
|
|
96
|
+
* than treating the tree as empty (#3243 review).
|
|
97
|
+
*/
|
|
98
|
+
function collectLiveXbriefEnvelopePaths(projectRoot, seams) {
|
|
99
|
+
const migratedRoot = join(projectRoot, MIGRATED_ARTIFACT_DIR);
|
|
100
|
+
const paths = [];
|
|
101
|
+
const enumFailures = [];
|
|
102
|
+
const definitionPath = join(migratedRoot, `PROJECT-DEFINITION${MIGRATED_ARTIFACT_SUFFIX}`);
|
|
103
|
+
// Existence must not require a successful read/stat — unreadable definitions
|
|
104
|
+
// still enter the scan so the fail-closed unreadable branch can fire
|
|
105
|
+
// (#3243 review: ENOENT = absent; EACCES/EPERM/other = include).
|
|
106
|
+
const readdir = seams.readdir ?? readdirSync;
|
|
107
|
+
const definitionPresence = probeFilePresence(definitionPath, seams);
|
|
108
|
+
if (definitionPresence === "present" || definitionPresence === "unreadable") {
|
|
109
|
+
paths.push(definitionPath);
|
|
110
|
+
}
|
|
111
|
+
for (const folder of ENVELOPE_MAJOR_SCAN_FOLDERS) {
|
|
112
|
+
const dir = join(migratedRoot, folder);
|
|
113
|
+
const dirPresence = probeDirectoryPresence(dir, seams);
|
|
114
|
+
if (dirPresence === "absent") {
|
|
115
|
+
// Clean absence (ENOENT / seam false) — greenfield-ok for that folder.
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (dirPresence === "unreadable") {
|
|
119
|
+
// Stat failed with non-ENOENT before readdir — fail closed (do not pretend empty).
|
|
120
|
+
enumFailures.push(relative(projectRoot, dir).replace(/\\/g, "/"));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
let names;
|
|
124
|
+
try {
|
|
125
|
+
names = readdir(dir);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Dir exists but cannot be listed — fail closed via synthetic scan entry
|
|
129
|
+
// (directory path; no invented filenames).
|
|
130
|
+
enumFailures.push(relative(projectRoot, dir).replace(/\\/g, "/"));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const name of names) {
|
|
134
|
+
if (name.endsWith(MIGRATED_ARTIFACT_SUFFIX)) {
|
|
135
|
+
paths.push(join(dir, name));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { paths, enumFailures };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Scan live project xbrief envelopes (PROJECT-DEFINITION + pending/active) for
|
|
143
|
+
* schema distance vs the installed framework envelope major (#3243). Complements
|
|
144
|
+
* #2971 PROJECT-DEFINITION-only probe; remediation aligns with migrate hybrid
|
|
145
|
+
* rewrite (#3236).
|
|
146
|
+
*/
|
|
147
|
+
export function scanXbriefEnvelopeVersions(projectRoot, seams = {}, targetVersion = VBRIEF_VERSION) {
|
|
148
|
+
const { paths, enumFailures } = collectLiveXbriefEnvelopePaths(projectRoot, seams);
|
|
149
|
+
const entries = [];
|
|
150
|
+
// Existing pending/active dirs that cannot be enumerated fail closed as
|
|
151
|
+
// behind-major (null declared) — same class as unreadable files (#3243 review).
|
|
152
|
+
for (const relativePath of enumFailures) {
|
|
153
|
+
entries.push({
|
|
154
|
+
relativePath,
|
|
155
|
+
declaredVersion: null,
|
|
156
|
+
distance: "behind-major",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
for (const filePath of paths) {
|
|
160
|
+
const relativePath = relative(projectRoot, filePath).replace(/\\/g, "/");
|
|
161
|
+
const text = readText(filePath, seams);
|
|
162
|
+
// Unreadable / malformed live envelopes fail closed as behind-major (null
|
|
163
|
+
// declared) so Doctor cannot skip to no-envelopes/pass (#3243 review).
|
|
164
|
+
if (text === null) {
|
|
165
|
+
entries.push({
|
|
166
|
+
relativePath,
|
|
167
|
+
declaredVersion: null,
|
|
168
|
+
distance: "behind-major",
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
let declared = null;
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(text);
|
|
175
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
176
|
+
declared = readDeclaredArtifactVersion(parsed);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
declared = null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
declared = null;
|
|
184
|
+
}
|
|
185
|
+
const distance = classifyXbriefSchemaDistance(declared, targetVersion);
|
|
186
|
+
entries.push({
|
|
187
|
+
relativePath,
|
|
188
|
+
declaredVersion: declared,
|
|
189
|
+
distance,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const behindMajor = entries.filter((e) => e.distance === "behind-major");
|
|
193
|
+
let worstDistance = null;
|
|
194
|
+
if (behindMajor.length > 0) {
|
|
195
|
+
worstDistance = "behind-major";
|
|
196
|
+
}
|
|
197
|
+
else if (entries.some((e) => e.distance === "behind-minor")) {
|
|
198
|
+
worstDistance = "behind-minor";
|
|
199
|
+
}
|
|
200
|
+
else if (entries.length > 0) {
|
|
201
|
+
worstDistance = "current";
|
|
202
|
+
}
|
|
203
|
+
return { targetVersion, entries, worstDistance, behindMajor };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Fail closed when any scanned project xBRIEF envelope declares a major behind
|
|
207
|
+
* the framework target (#3243 / epic #3237 Q5 Option 2). Behind-minor does not
|
|
208
|
+
* fail this check. Remediation: `deft migrate:xbrief` (rewrites hybrid 0.6 on
|
|
209
|
+
* already-xbrief trees — #3236). Distinct from `stale-xbrief-schema-deposit`
|
|
210
|
+
* which routes schema-only drift to `directive update`.
|
|
211
|
+
*/
|
|
212
|
+
export function checkXbriefEnvelopeMajorVersion(projectRoot, seams = {}) {
|
|
213
|
+
const checkName = XBRIEF_ENVELOPE_MAJOR_CHECK;
|
|
214
|
+
const targetVersion = VBRIEF_VERSION;
|
|
215
|
+
const migratedRoot = join(projectRoot, MIGRATED_ARTIFACT_DIR);
|
|
216
|
+
const legacyRoot = join(projectRoot, LEGACY_ARTIFACT_DIR);
|
|
217
|
+
const hasMigrated = isDirectoryPath(migratedRoot, seams);
|
|
218
|
+
const hasLegacy = isDirectoryPath(legacyRoot, seams);
|
|
219
|
+
// Pure legacy vbrief/ trees need layout migrate first; skip rather than
|
|
220
|
+
// false-positive on missing xbrief envelopes.
|
|
221
|
+
if (hasLegacy && !hasMigrated) {
|
|
222
|
+
return {
|
|
223
|
+
name: checkName,
|
|
224
|
+
status: "skip",
|
|
225
|
+
detail: "Legacy-only layout (use layout migrate first); envelope major check skipped.",
|
|
226
|
+
data: { reason: "legacy-only-layout", target_version: targetVersion },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const scan = scanXbriefEnvelopeVersions(projectRoot, seams, targetVersion);
|
|
230
|
+
if (scan.entries.length === 0) {
|
|
231
|
+
return {
|
|
232
|
+
name: checkName,
|
|
233
|
+
status: "skip",
|
|
234
|
+
detail: "No project xBRIEF envelopes scanned (greenfield or empty lifecycle).",
|
|
235
|
+
data: {
|
|
236
|
+
reason: "no-envelopes",
|
|
237
|
+
target_version: targetVersion,
|
|
238
|
+
scanned: 0,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Fail closed for behind-major live envelopes (#3243 / #3236):
|
|
243
|
+
// - exact 0.6 → migratable via `deft migrate:xbrief`
|
|
244
|
+
// - null / 0.5 / unreadable / … → structural rewrite (migrate does not clear)
|
|
245
|
+
// When both classes are present, report BOTH remediations in one fail so
|
|
246
|
+
// migrate-only guidance cannot hide the non-migratable repair (#3243 review).
|
|
247
|
+
const migratableBehindMajor = scan.behindMajor.filter((e) => e.declaredVersion === LEGACY_VBRIEF_VERSION);
|
|
248
|
+
const nonMigratableBehindMajor = scan.behindMajor.filter((e) => e.declaredVersion !== LEGACY_VBRIEF_VERSION);
|
|
249
|
+
if (migratableBehindMajor.length > 0 && nonMigratableBehindMajor.length > 0) {
|
|
250
|
+
const migSample = migratableBehindMajor.slice(0, 3);
|
|
251
|
+
const nonSample = nonMigratableBehindMajor.slice(0, 3);
|
|
252
|
+
const nonDeclared = [
|
|
253
|
+
...new Set(nonSample.map((e) => e.declaredVersion ?? "missing/unreadable")),
|
|
254
|
+
].join(", ");
|
|
255
|
+
return {
|
|
256
|
+
name: checkName,
|
|
257
|
+
status: "fail",
|
|
258
|
+
detail: `behind-major (mixed) -- framework ${targetVersion}: ` +
|
|
259
|
+
`${migratableBehindMajor.length} migratable (${LEGACY_VBRIEF_VERSION}: ` +
|
|
260
|
+
`${migSample.map((e) => e.relativePath).join(", ")}) and ` +
|
|
261
|
+
`${nonMigratableBehindMajor.length} non-migratable (declared ${nonDeclared}: ` +
|
|
262
|
+
`${nonSample.map((e) => e.relativePath).join(", ")}). ` +
|
|
263
|
+
`Next: (1) run \`${XBRIEF_ENVELOPE_MIGRATE_COMMAND}\` for exact ${LEGACY_VBRIEF_VERSION}; ` +
|
|
264
|
+
`(2) rewrite non-migratable live envelopes to full xBRIEFInfo@${targetVersion} structure ` +
|
|
265
|
+
`(not version-only; migrate does not clear them).`,
|
|
266
|
+
data: {
|
|
267
|
+
status: "behind-major-mixed",
|
|
268
|
+
target_version: targetVersion,
|
|
269
|
+
migratable_count: migratableBehindMajor.length,
|
|
270
|
+
non_migratable_count: nonMigratableBehindMajor.length,
|
|
271
|
+
behind_major_count: scan.behindMajor.length,
|
|
272
|
+
sample_paths: [
|
|
273
|
+
...migSample.map((e) => e.relativePath),
|
|
274
|
+
...nonSample.map((e) => e.relativePath),
|
|
275
|
+
],
|
|
276
|
+
next_command: XBRIEF_ENVELOPE_MIGRATE_COMMAND,
|
|
277
|
+
suggestion: `${XBRIEF_ENVELOPE_MIGRATE_COMMAND} for 0.6; rewrite non-migratable to full ` +
|
|
278
|
+
`xBRIEFInfo@${targetVersion} structure (not version-only)`,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (migratableBehindMajor.length > 0) {
|
|
283
|
+
const sample = migratableBehindMajor.slice(0, 5);
|
|
284
|
+
const samplePaths = sample.map((e) => e.relativePath).join(", ");
|
|
285
|
+
const more = migratableBehindMajor.length > sample.length
|
|
286
|
+
? ` (+${migratableBehindMajor.length - sample.length} more)`
|
|
287
|
+
: "";
|
|
288
|
+
return {
|
|
289
|
+
name: checkName,
|
|
290
|
+
status: "fail",
|
|
291
|
+
detail: `behind-major -- declared ${LEGACY_VBRIEF_VERSION}, framework ${targetVersion} ` +
|
|
292
|
+
`(${migratableBehindMajor.length} artifact(s): ${samplePaths}${more}). ` +
|
|
293
|
+
`Next action: run \`${XBRIEF_ENVELOPE_MIGRATE_COMMAND}\` to bump project JSON envelopes ` +
|
|
294
|
+
`to xBRIEFInfo@${targetVersion} (layout rename alone is not enough; #3236 rewrites hybrid 0.6 in place).`,
|
|
295
|
+
data: {
|
|
296
|
+
status: "behind-major",
|
|
297
|
+
declared_versions: sample.map((e) => e.declaredVersion),
|
|
298
|
+
target_version: targetVersion,
|
|
299
|
+
behind_major_count: migratableBehindMajor.length,
|
|
300
|
+
sample_paths: sample.map((e) => e.relativePath),
|
|
301
|
+
next_command: XBRIEF_ENVELOPE_MIGRATE_COMMAND,
|
|
302
|
+
suggestion: XBRIEF_ENVELOPE_MIGRATE_COMMAND,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (nonMigratableBehindMajor.length > 0) {
|
|
307
|
+
// Fail closed without claiming migrate:xbrief alone clears these — that verb
|
|
308
|
+
// rewrites exact 0.6 only (#3236). Give executable next actions per class
|
|
309
|
+
// (permissions / re-emit / delete-replace) so Doctor is not permanently blocked
|
|
310
|
+
// after a non-applicable remediation (#3243 review).
|
|
311
|
+
const sample = nonMigratableBehindMajor.slice(0, 5);
|
|
312
|
+
const declaredVersions = [
|
|
313
|
+
...new Set(sample.map((e) => e.declaredVersion ?? "missing/unreadable")),
|
|
314
|
+
].join(", ");
|
|
315
|
+
const samplePaths = sample.map((e) => e.relativePath).join(", ");
|
|
316
|
+
const more = nonMigratableBehindMajor.length > sample.length
|
|
317
|
+
? ` (+${nonMigratableBehindMajor.length - sample.length} more)`
|
|
318
|
+
: "";
|
|
319
|
+
return {
|
|
320
|
+
name: checkName,
|
|
321
|
+
status: "fail",
|
|
322
|
+
detail: `behind-major (non-migratable) -- declared ${declaredVersions}, framework ${targetVersion} ` +
|
|
323
|
+
`(${nonMigratableBehindMajor.length} artifact(s): ${samplePaths}${more}). ` +
|
|
324
|
+
`Next actions: (1) if a path is unreadable, fix FS permissions then re-run doctor; ` +
|
|
325
|
+
`(2) re-emit full xBRIEFInfo@${targetVersion} via scope tools ` +
|
|
326
|
+
`(\`${XBRIEF_ENVELOPE_MIGRATE_COMMAND}\` only when declared is exact ${LEGACY_VBRIEF_VERSION}); ` +
|
|
327
|
+
`(3) delete or replace invalid lifecycle artifacts after human confirm — ` +
|
|
328
|
+
`do not only bump the version field.`,
|
|
329
|
+
data: {
|
|
330
|
+
status: "behind-major-non-migratable",
|
|
331
|
+
declared_versions: sample.map((e) => e.declaredVersion),
|
|
332
|
+
target_version: targetVersion,
|
|
333
|
+
behind_major_count: nonMigratableBehindMajor.length,
|
|
334
|
+
sample_paths: sample.map((e) => e.relativePath),
|
|
335
|
+
next_command: null,
|
|
336
|
+
suggestion: `fix FS permissions if unreadable; re-emit full xBRIEFInfo@${targetVersion} via scope tools ` +
|
|
337
|
+
`(migrate:xbrief only for exact ${LEGACY_VBRIEF_VERSION}); or delete/replace invalid artifacts after human confirm (not version-only)`,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// current or behind-minor: this check is major-only (#3243).
|
|
342
|
+
const declaredSummary = scan.entries
|
|
343
|
+
.map((e) => e.declaredVersion)
|
|
344
|
+
.filter((v) => typeof v === "string")
|
|
345
|
+
.slice(0, 3)
|
|
346
|
+
.join(", ") || targetVersion;
|
|
347
|
+
return {
|
|
348
|
+
name: checkName,
|
|
349
|
+
status: "pass",
|
|
350
|
+
detail: `current -- scanned ${scan.entries.length} envelope(s) at framework major ` +
|
|
351
|
+
`(declared sample ${declaredSummary}; framework ${targetVersion})` +
|
|
352
|
+
(scan.worstDistance === "behind-minor" ? "; behind-minor is non-failing for this check" : ""),
|
|
353
|
+
data: {
|
|
354
|
+
status: scan.worstDistance ?? "current",
|
|
355
|
+
target_version: targetVersion,
|
|
356
|
+
scanned: scan.entries.length,
|
|
357
|
+
worst_distance: scan.worstDistance,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
19
361
|
export function checkQuickStartResolves(projectRoot, installRoot, seams = {}) {
|
|
20
362
|
if (installRoot === null) {
|
|
21
363
|
return {
|
|
@@ -734,6 +1076,82 @@ export function checkGitignoreCoverage(projectRoot, seams = {}) {
|
|
|
734
1076
|
* Decided-off is quiet; dismiss-with-reason is pass with reason in detail.
|
|
735
1077
|
* Invalid typed blocks resolve fail-closed and surface via source=default-on-error.
|
|
736
1078
|
*/
|
|
1079
|
+
/**
|
|
1080
|
+
* Fail closed when completed/ plan.status disagrees with the folder (#3242 AC1 / epic #3237 Q4).
|
|
1081
|
+
* Open plan.items under completed/ are reported by `checkCompletedOpenItems` (exit-exempt
|
|
1082
|
+
* until historical corpora are backfilled; scope:complete enforces items hard).
|
|
1083
|
+
*/
|
|
1084
|
+
export function checkCompletedLifecycleConsistency(projectRoot) {
|
|
1085
|
+
const checkName = "completed-lifecycle-consistency";
|
|
1086
|
+
const result = scanCompletedLifecycleConsistency(projectRoot);
|
|
1087
|
+
// Hard fail: folder/status drift and unreadable completed artifacts (not open items).
|
|
1088
|
+
const hardFindings = result.findings.filter((f) => f.kind === "status_mismatch" || f.kind === "unreadable");
|
|
1089
|
+
if (hardFindings.length === 0) {
|
|
1090
|
+
return {
|
|
1091
|
+
name: checkName,
|
|
1092
|
+
status: "pass",
|
|
1093
|
+
detail: "Completed folder/status consistency OK (#3242): no plan.status drift under completed/",
|
|
1094
|
+
data: {
|
|
1095
|
+
finding_count: 0,
|
|
1096
|
+
open_items_deferred_to: "completed-open-items",
|
|
1097
|
+
},
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
const message = `Completed lifecycle folder/status consistency failed (#3242). ` +
|
|
1101
|
+
`${hardFindings.length} finding(s):\n` +
|
|
1102
|
+
hardFindings.map((f) => ` - ${f.detail}`).join("\n") +
|
|
1103
|
+
`\nRule: completed/ requires plan.status in [completed|failed]; artifacts must be readable.`;
|
|
1104
|
+
return {
|
|
1105
|
+
name: checkName,
|
|
1106
|
+
status: "fail",
|
|
1107
|
+
detail: message,
|
|
1108
|
+
data: {
|
|
1109
|
+
finding_count: hardFindings.length,
|
|
1110
|
+
findings: hardFindings.map((f) => ({
|
|
1111
|
+
rel_path: f.relPath,
|
|
1112
|
+
plan_status: f.planStatus,
|
|
1113
|
+
kind: f.kind,
|
|
1114
|
+
detail: f.detail,
|
|
1115
|
+
})),
|
|
1116
|
+
},
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Surface non-terminal plan.items under completed/ (#3242). Exit-exempt so
|
|
1121
|
+
* historical pre-#2862 corpora do not red-light doctor; scope:complete fails closed.
|
|
1122
|
+
*/
|
|
1123
|
+
export function checkCompletedOpenItems(projectRoot) {
|
|
1124
|
+
const checkName = "completed-open-items";
|
|
1125
|
+
const result = scanCompletedLifecycleConsistency(projectRoot);
|
|
1126
|
+
const openFindings = result.findings.filter((f) => f.kind === "open_items");
|
|
1127
|
+
if (openFindings.length === 0) {
|
|
1128
|
+
return {
|
|
1129
|
+
name: checkName,
|
|
1130
|
+
status: "pass",
|
|
1131
|
+
detail: "Completed open-items consistency OK (#3242): no non-terminal plan.items under completed/",
|
|
1132
|
+
data: { finding_count: 0 },
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
const message = `Completed lifecycle open plan.items (#3242, advisory). ` +
|
|
1136
|
+
`${openFindings.length} artifact(s) under completed/ still list non-terminal items:\n` +
|
|
1137
|
+
openFindings.map((f) => ` - ${f.detail}`).join("\n") +
|
|
1138
|
+
`\nscope:complete fails closed on this class; historical corpus is advisory until backfill.`;
|
|
1139
|
+
return {
|
|
1140
|
+
name: checkName,
|
|
1141
|
+
status: "fail",
|
|
1142
|
+
detail: message,
|
|
1143
|
+
data: {
|
|
1144
|
+
finding_count: openFindings.length,
|
|
1145
|
+
advisory: true,
|
|
1146
|
+
findings: openFindings.map((f) => ({
|
|
1147
|
+
rel_path: f.relPath,
|
|
1148
|
+
plan_status: f.planStatus,
|
|
1149
|
+
kind: f.kind,
|
|
1150
|
+
detail: f.detail,
|
|
1151
|
+
})),
|
|
1152
|
+
},
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
737
1155
|
export function checkCoverageCheckResumePolicy(projectRoot) {
|
|
738
1156
|
const debt = resolveCoverageDebt(projectRoot);
|
|
739
1157
|
const resume = resolveCheckResume(projectRoot);
|
|
@@ -813,6 +1231,8 @@ export function deriveExitCode(checks, errors) {
|
|
|
813
1231
|
"stale-xbrief-schema-deposit",
|
|
814
1232
|
"typescript-7-side-by-side",
|
|
815
1233
|
"coverage-check-resume-policy",
|
|
1234
|
+
// Historical pre-#2862 completed/ corpora; hard fail is scope:complete (#3242).
|
|
1235
|
+
"completed-open-items",
|
|
816
1236
|
]);
|
|
817
1237
|
if (errors.length > 0 || checks.some((c) => c.status === "error")) {
|
|
818
1238
|
return 2;
|
|
@@ -853,9 +1273,12 @@ export function runChecksImpl(projectRoot, seams = {}) {
|
|
|
853
1273
|
checks.push(checkLegacyLayout(projectRoot, seams));
|
|
854
1274
|
checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
|
|
855
1275
|
checks.push(checkStaleXbriefSchemaDeposit(projectRoot, seams));
|
|
1276
|
+
checks.push(checkXbriefEnvelopeMajorVersion(projectRoot, seams));
|
|
856
1277
|
checks.push(checkGitignoreCoverage(projectRoot, seams));
|
|
857
1278
|
checks.push(checkTypescript7SideBySide(projectRoot, seams));
|
|
858
1279
|
checks.push(checkCoverageCheckResumePolicy(projectRoot));
|
|
1280
|
+
checks.push(checkCompletedLifecycleConsistency(projectRoot));
|
|
1281
|
+
checks.push(checkCompletedOpenItems(projectRoot));
|
|
859
1282
|
return {
|
|
860
1283
|
projectRoot,
|
|
861
1284
|
installRoot: null,
|
|
@@ -872,9 +1295,12 @@ export function runChecksImpl(projectRoot, seams = {}) {
|
|
|
872
1295
|
checks.push(checkLegacyLayout(projectRoot, seams));
|
|
873
1296
|
checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
|
|
874
1297
|
checks.push(checkStaleXbriefSchemaDeposit(projectRoot, seams));
|
|
1298
|
+
checks.push(checkXbriefEnvelopeMajorVersion(projectRoot, seams));
|
|
875
1299
|
checks.push(checkGitignoreCoverage(projectRoot, seams));
|
|
876
1300
|
checks.push(checkTypescript7SideBySide(projectRoot, seams));
|
|
877
1301
|
checks.push(checkCoverageCheckResumePolicy(projectRoot));
|
|
1302
|
+
checks.push(checkCompletedLifecycleConsistency(projectRoot));
|
|
1303
|
+
checks.push(checkCompletedOpenItems(projectRoot));
|
|
878
1304
|
return {
|
|
879
1305
|
projectRoot,
|
|
880
1306
|
installRoot,
|
package/dist/doctor/main.d.ts
CHANGED
|
@@ -5,13 +5,12 @@ export declare function cmdDoctor(args: readonly string[], seams?: DoctorSeams):
|
|
|
5
5
|
export declare function runAgentHooksHealthCheck(projectRoot: string, consumerContext: boolean, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): boolean;
|
|
6
6
|
export declare function runAgentHooksLiveProbeCheck(projectRoot: string, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): void;
|
|
7
7
|
/**
|
|
8
|
-
* Fail closed when
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* (
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* must NOT route to migrate:xbrief when only framework schema files are stale.
|
|
8
|
+
* Fail closed when any scanned project xbrief lifecycle `*.xbrief.json` envelope
|
|
9
|
+
* declares a schema major behind the framework target (#2971 / #3243). Greenfield
|
|
10
|
+
* (no envelopes) skips. Behind-major records `deft migrate:xbrief` as the next
|
|
11
|
+
* action (hybrid in-place rewrite on already-xbrief trees — #3236). Distinct from
|
|
12
|
+
* deposited-schema (`stale-xbrief-schema-deposit`) which must NOT route to
|
|
13
|
+
* migrate:xbrief when only framework schema files are stale.
|
|
15
14
|
*/
|
|
16
15
|
export declare function runXbriefEnvelopeVersionCheck(projectRoot: string, sink: ReturnType<typeof createPlainSink>, addFinding: (f: Finding) => void, seams: DoctorSeams): void;
|
|
17
16
|
/**
|