@deftai/directive-core 0.102.0 → 0.104.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/classify.js +167 -4
- package/dist/check/gate-lists.d.ts +3 -2
- package/dist/check/gate-lists.js +5 -2
- package/dist/check/index.d.ts +1 -0
- package/dist/check/index.js +1 -0
- package/dist/check/session-completed-ac.d.ts +39 -0
- package/dist/check/session-completed-ac.js +198 -0
- package/dist/doctor/main.js +12 -7
- package/dist/fs/contained-write.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/hygiene.d.ts +7 -1
- package/dist/init-deposit/hygiene.js +11 -3
- package/dist/init-deposit/scaffold.d.ts +12 -0
- package/dist/init-deposit/scaffold.js +81 -8
- package/dist/intake/clause-derivation.d.ts +37 -0
- package/dist/intake/clause-derivation.js +207 -0
- package/dist/intake/index.d.ts +1 -0
- package/dist/intake/index.js +1 -0
- package/dist/intake/issue-ingest.js +2 -38
- package/dist/preflight/evaluate.d.ts +5 -0
- package/dist/preflight/evaluate.js +14 -0
- package/dist/preflight/index.d.ts +1 -1
- package/dist/product-first-done-gate/empty-resolution.d.ts +1 -1
- package/dist/product-first-done-gate/empty-resolution.js +1 -1
- package/dist/product-first-done-gate/evaluate.d.ts +29 -0
- package/dist/product-first-done-gate/evaluate.js +110 -23
- package/dist/product-first-done-gate/index.d.ts +1 -1
- package/dist/product-first-done-gate/index.js +1 -1
- package/dist/run-summary/emit.d.ts +42 -2
- package/dist/run-summary/emit.js +250 -25
- package/dist/run-summary/index.d.ts +1 -1
- package/dist/run-summary/index.js +1 -1
- package/dist/run-summary/share.d.ts +1 -1
- package/dist/run-summary/share.js +2 -2
- package/dist/run-summary/types.d.ts +10 -3
- package/dist/scope/acceptance-activate-gate.d.ts +1 -1
- package/dist/scope/acceptance-activate-gate.js +4 -4
- package/dist/scope/acceptance-evidence.d.ts +15 -0
- package/dist/scope/acceptance-evidence.js +39 -0
- package/dist/scope/capacity-stamp.d.ts +2 -0
- package/dist/scope/capacity-stamp.js +4 -0
- package/dist/scope/delivery-evidence.d.ts +6 -0
- package/dist/scope/delivery-evidence.js +17 -0
- package/dist/scope/transition.js +66 -4
- package/dist/session/ceremony-dial-evidence.d.ts +42 -0
- package/dist/session/ceremony-dial-evidence.js +194 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/session-start.js +21 -2
- package/dist/slice/existing.js +12 -1
- package/dist/slice/record.d.ts +8 -5
- package/dist/slice/record.js +21 -13
- package/dist/telemetry-coverage/evaluate.d.ts +42 -0
- package/dist/telemetry-coverage/evaluate.js +148 -0
- package/dist/telemetry-coverage/fake-trial.d.ts +45 -0
- package/dist/telemetry-coverage/fake-trial.js +194 -0
- package/dist/telemetry-coverage/index.d.ts +5 -0
- package/dist/telemetry-coverage/index.js +5 -0
- package/dist/telemetry-coverage/kinds.d.ts +19 -0
- package/dist/telemetry-coverage/kinds.js +52 -0
- package/dist/telemetry-coverage/scan-callers.d.ts +25 -0
- package/dist/telemetry-coverage/scan-callers.js +168 -0
- package/dist/vbrief-reconcile/index.d.ts +2 -0
- package/dist/vbrief-reconcile/index.js +1 -0
- package/dist/vbrief-reconcile/origin-freshness.d.ts +44 -0
- package/dist/vbrief-reconcile/origin-freshness.js +221 -0
- package/dist/verify-ac/evaluate.d.ts +18 -0
- package/dist/verify-ac/evaluate.js +24 -2
- package/dist/verify-ac/index.d.ts +1 -1
- package/dist/verify-ac/index.js +1 -1
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/package.json +3 -3
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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")) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-authored briefs run #3323 clause derivation on activate/promote (#3360).
|
|
3
|
+
*
|
|
4
|
+
* issue:ingest already stamps clauses. The dominant consumer path authors the
|
|
5
|
+
* brief by hand; #3334 only required plan.acceptance to exist. This module is
|
|
6
|
+
* the missing derive step.
|
|
7
|
+
*/
|
|
8
|
+
import { type AcceptanceClause } from "../verify-ac/clauses.js";
|
|
9
|
+
/** True when activate/promote must run #3323 (absent, empty none_stated, or command-only). */
|
|
10
|
+
export declare function needsClauseDerivation(acceptance: unknown): boolean;
|
|
11
|
+
/** Task-statement text from title, narratives, and item Acceptance fields. */
|
|
12
|
+
export declare function collectTaskStatementFromPlan(plan: Record<string, unknown>): string;
|
|
13
|
+
export interface ClauseDerivationResult {
|
|
14
|
+
readonly applied: boolean;
|
|
15
|
+
readonly clauses: readonly AcceptanceClause[];
|
|
16
|
+
readonly notice: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Stamp derived clauses onto plan.acceptance in place. Headless: records
|
|
20
|
+
* chosen_reading, never blocks on a question.
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyClauseDerivationToPlan(plan: Record<string, unknown>, options?: {
|
|
23
|
+
readonly projectRoot?: string;
|
|
24
|
+
readonly emitStamp?: boolean;
|
|
25
|
+
}): ClauseDerivationResult;
|
|
26
|
+
/** Stable fingerprint of plan.acceptance for first-write / material-change detection (#3355). */
|
|
27
|
+
export declare function acceptanceFingerprint(acceptance: unknown): string | null;
|
|
28
|
+
/** True when next is a first write or a material change versus previous (#3355). */
|
|
29
|
+
export declare function isMaterialAcceptanceChange(previous: unknown, next: unknown): boolean;
|
|
30
|
+
/** Fail-open acceptance_stamp emission (same contract as issue:ingest / #3355). */
|
|
31
|
+
export declare function emitAcceptanceStampFromPlan(projectRoot: string, plan: unknown, env?: NodeJS.ProcessEnv): void;
|
|
32
|
+
/**
|
|
33
|
+
* Emit acceptance_stamp when plan.acceptance is first written or materially changed.
|
|
34
|
+
* State-observed: callers pass the before/after blocks from any lifecycle verb (#3355).
|
|
35
|
+
*/
|
|
36
|
+
export declare function maybeEmitAcceptanceStampFromChange(projectRoot: string, previous: unknown, next: unknown, env?: NodeJS.ProcessEnv): boolean;
|
|
37
|
+
//# sourceMappingURL=clause-derivation.d.ts.map
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-authored briefs run #3323 clause derivation on activate/promote (#3360).
|
|
3
|
+
*
|
|
4
|
+
* issue:ingest already stamps clauses. The dominant consumer path authors the
|
|
5
|
+
* brief by hand; #3334 only required plan.acceptance to exist. This module is
|
|
6
|
+
* the missing derive step.
|
|
7
|
+
*/
|
|
8
|
+
import { ENV_RUN_SUMMARY_PATH, RunSummaryEmitter } from "../run-summary/index.js";
|
|
9
|
+
import { deriveAcceptanceClauses, readAcceptanceClauses, serializeAcceptanceClauses, } from "../verify-ac/clauses.js";
|
|
10
|
+
const NARRATIVE_KEYS = [
|
|
11
|
+
"Overview",
|
|
12
|
+
"Description",
|
|
13
|
+
"Acceptance",
|
|
14
|
+
"AcceptanceCriteria",
|
|
15
|
+
"Acceptance sketch",
|
|
16
|
+
"AcceptanceSketch",
|
|
17
|
+
"Test",
|
|
18
|
+
"Verification",
|
|
19
|
+
"ImplementationPlan",
|
|
20
|
+
];
|
|
21
|
+
function asRecord(value) {
|
|
22
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function isNonEmptyString(value) {
|
|
28
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
29
|
+
}
|
|
30
|
+
/** True when activate/promote must run #3323 (absent, empty none_stated, or command-only). */
|
|
31
|
+
export function needsClauseDerivation(acceptance) {
|
|
32
|
+
if (acceptance === undefined || acceptance === null) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const rec = asRecord(acceptance);
|
|
36
|
+
if (rec === null) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return !(Array.isArray(rec.clauses) && rec.clauses.length > 0);
|
|
40
|
+
}
|
|
41
|
+
/** Task-statement text from title, narratives, and item Acceptance fields. */
|
|
42
|
+
export function collectTaskStatementFromPlan(plan) {
|
|
43
|
+
const parts = [];
|
|
44
|
+
if (isNonEmptyString(plan.title)) {
|
|
45
|
+
parts.push(plan.title.trim());
|
|
46
|
+
}
|
|
47
|
+
const narratives = asRecord(plan.narratives);
|
|
48
|
+
if (narratives !== null) {
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
for (const key of NARRATIVE_KEYS) {
|
|
51
|
+
const value = narratives[key];
|
|
52
|
+
if (isNonEmptyString(value)) {
|
|
53
|
+
parts.push(value.trim());
|
|
54
|
+
seen.add(key.toLowerCase());
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const [key, value] of Object.entries(narratives)) {
|
|
58
|
+
if (seen.has(key.toLowerCase()) || !isNonEmptyString(value)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
parts.push(value.trim());
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(plan.items)) {
|
|
65
|
+
for (const item of plan.items) {
|
|
66
|
+
const rec = asRecord(item);
|
|
67
|
+
if (rec === null) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const narrative = asRecord(rec.narrative);
|
|
71
|
+
const text = narrative?.Acceptance;
|
|
72
|
+
if (isNonEmptyString(text)) {
|
|
73
|
+
parts.push(text.trim());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return parts.join("\n\n");
|
|
78
|
+
}
|
|
79
|
+
function formatAmbiguousClauseNotice(clauses) {
|
|
80
|
+
const flagged = clauses.filter((clause) => clause.ambiguous);
|
|
81
|
+
const lines = [
|
|
82
|
+
`#3323 clause derivation stamped ${clauses.length} clause(s)` +
|
|
83
|
+
(flagged.length > 0 ? `; flagged-ambiguous: ${flagged.length}` : ""),
|
|
84
|
+
];
|
|
85
|
+
for (const clause of flagged) {
|
|
86
|
+
const chosen = clause.chosen_reading ?? 0;
|
|
87
|
+
const chosenPath = clause.readings?.[chosen]?.artifact_path ?? clause.artifact_path ?? "(none)";
|
|
88
|
+
lines.push(` clause ${clause.id}: two readings; chosen_reading=${chosen} (${chosenPath}) [headless #3323]`);
|
|
89
|
+
for (const [index, reading] of (clause.readings ?? []).entries()) {
|
|
90
|
+
lines.push(` reading ${index}: ${reading.artifact_path ?? "(none)"}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Stamp derived clauses onto plan.acceptance in place. Headless: records
|
|
97
|
+
* chosen_reading, never blocks on a question.
|
|
98
|
+
*/
|
|
99
|
+
export function applyClauseDerivationToPlan(plan, options = {}) {
|
|
100
|
+
if (!needsClauseDerivation(plan.acceptance)) {
|
|
101
|
+
return {
|
|
102
|
+
applied: false,
|
|
103
|
+
clauses: readAcceptanceClauses(plan.acceptance),
|
|
104
|
+
notice: "",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const clauses = deriveAcceptanceClauses(collectTaskStatementFromPlan(plan));
|
|
108
|
+
if (clauses.length === 0) {
|
|
109
|
+
return { applied: false, clauses: [], notice: "" };
|
|
110
|
+
}
|
|
111
|
+
const existing = asRecord(plan.acceptance);
|
|
112
|
+
const commands = existing !== null && Array.isArray(existing.commands) ? existing.commands : [];
|
|
113
|
+
const hasCommands = commands.length > 0;
|
|
114
|
+
const noneStated = hasCommands ? existing?.none_stated === true : true;
|
|
115
|
+
const sourceRung = hasCommands && existing?.none_stated !== true
|
|
116
|
+
? isNonEmptyString(existing?.source_rung)
|
|
117
|
+
? existing.source_rung
|
|
118
|
+
: "stated"
|
|
119
|
+
: "derived";
|
|
120
|
+
plan.acceptance = {
|
|
121
|
+
...(existing ?? {}),
|
|
122
|
+
commands: hasCommands ? commands : [],
|
|
123
|
+
none_stated: noneStated,
|
|
124
|
+
source_rung: sourceRung,
|
|
125
|
+
derived_reason: `derived ${clauses.length} independently testable clauses from the task statement before product edit (#3323)`,
|
|
126
|
+
clauses: serializeAcceptanceClauses(clauses),
|
|
127
|
+
};
|
|
128
|
+
if (options.emitStamp !== false && options.projectRoot !== undefined) {
|
|
129
|
+
emitAcceptanceStampFromPlan(options.projectRoot, plan);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
applied: true,
|
|
133
|
+
clauses,
|
|
134
|
+
notice: formatAmbiguousClauseNotice(clauses),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Stable fingerprint of plan.acceptance for first-write / material-change detection (#3355). */
|
|
138
|
+
export function acceptanceFingerprint(acceptance) {
|
|
139
|
+
const rec = asRecord(acceptance);
|
|
140
|
+
if (rec === null) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const commands = Array.isArray(rec.commands) ? rec.commands.length : 0;
|
|
144
|
+
const clauses = Array.isArray(rec.clauses)
|
|
145
|
+
? rec.clauses.map((entry, index) => {
|
|
146
|
+
const row = asRecord(entry);
|
|
147
|
+
if (row === null) {
|
|
148
|
+
return `${index}`;
|
|
149
|
+
}
|
|
150
|
+
return `${row.id ?? index}:${row.text ?? ""}:${row.artifact_path ?? ""}`;
|
|
151
|
+
})
|
|
152
|
+
: [];
|
|
153
|
+
const rung = typeof rec.source_rung === "string" ? rec.source_rung : "";
|
|
154
|
+
return `${rung}|${rec.none_stated === true}|${commands}|${clauses.join(";")}`;
|
|
155
|
+
}
|
|
156
|
+
/** True when next is a first write or a material change versus previous (#3355). */
|
|
157
|
+
export function isMaterialAcceptanceChange(previous, next) {
|
|
158
|
+
const nextFp = acceptanceFingerprint(next);
|
|
159
|
+
if (nextFp === null) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return acceptanceFingerprint(previous) !== nextFp;
|
|
163
|
+
}
|
|
164
|
+
/** Fail-open acceptance_stamp emission (same contract as issue:ingest / #3355). */
|
|
165
|
+
export function emitAcceptanceStampFromPlan(projectRoot, plan, env = process.env) {
|
|
166
|
+
const rec = asRecord(plan);
|
|
167
|
+
if (rec === null) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const acceptance = asRecord(rec.acceptance);
|
|
171
|
+
if (acceptance === null) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const dest = env[ENV_RUN_SUMMARY_PATH];
|
|
175
|
+
if (dest === undefined || dest.trim().length === 0) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const sessionId = typeof env.DEFT_SESSION_ID === "string" && env.DEFT_SESSION_ID.trim().length > 0
|
|
180
|
+
? env.DEFT_SESSION_ID.trim()
|
|
181
|
+
: "clause-derivation";
|
|
182
|
+
const emitter = new RunSummaryEmitter({ projectRoot, sessionId, env });
|
|
183
|
+
const commands = Array.isArray(acceptance.commands) ? acceptance.commands.length : 0;
|
|
184
|
+
const clauses = Array.isArray(acceptance.clauses) ? acceptance.clauses.length : 0;
|
|
185
|
+
emitter.emitAcceptanceStamp({
|
|
186
|
+
rung: typeof acceptance.source_rung === "string" ? acceptance.source_rung : "project_floor",
|
|
187
|
+
none_stated: acceptance.none_stated === true,
|
|
188
|
+
command_count: commands,
|
|
189
|
+
clause_count: clauses,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// fail-open
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Emit acceptance_stamp when plan.acceptance is first written or materially changed.
|
|
198
|
+
* State-observed: callers pass the before/after blocks from any lifecycle verb (#3355).
|
|
199
|
+
*/
|
|
200
|
+
export function maybeEmitAcceptanceStampFromChange(projectRoot, previous, next, env = process.env) {
|
|
201
|
+
if (!isMaterialAcceptanceChange(previous, next)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
emitAcceptanceStampFromPlan(projectRoot, { acceptance: next }, env);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=clause-derivation.js.map
|
package/dist/intake/index.d.ts
CHANGED
package/dist/intake/index.js
CHANGED
|
@@ -6,7 +6,6 @@ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/project
|
|
|
6
6
|
import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
|
|
7
7
|
import { captureAndAttachLiteralAcceptance } from "../literal-acceptance/index.js";
|
|
8
8
|
import { stampAcceptanceFromLiteralCapture } from "../product-first-done-gate/index.js";
|
|
9
|
-
import { ENV_RUN_SUMMARY_PATH, RunSummaryEmitter } from "../run-summary/index.js";
|
|
10
9
|
import { call } from "../scm/call.js";
|
|
11
10
|
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
12
11
|
import { resolveProjectRepo } from "../slice/project-context.js";
|
|
@@ -15,6 +14,7 @@ import { slugify, TODAY } from "../vbrief-build/build.js";
|
|
|
15
14
|
import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
|
|
16
15
|
import { stampDerivedClausesOnAcceptance } from "../verify-ac/clauses.js";
|
|
17
16
|
import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, VBRIEF_VERSION, } from "../xbrief-migrate/constants.js";
|
|
17
|
+
import { emitAcceptanceStampFromPlan } from "./clause-derivation.js";
|
|
18
18
|
import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, stripFencedCodeBlocks, } from "./markdown-scanners.js";
|
|
19
19
|
import { detectRepo, extractReferencesFromVbrief, fetchOpenIssues, GITHUB_ISSUE_REF_TYPES, LIFECYCLE_FOLDERS, parseIssueNumber, } from "./reconcile-issues.js";
|
|
20
20
|
/** Reference type pointing at the canonical current-shape comment permalink (#1870). */
|
|
@@ -723,45 +723,9 @@ export function ingestOne(issue, options) {
|
|
|
723
723
|
assertWriteTargetSafe(projectRoot, target);
|
|
724
724
|
mkdirSync(folderPath, { recursive: true });
|
|
725
725
|
writeFileSync(target, `${JSON.stringify(vbrief, null, 2)}\n`, "utf8");
|
|
726
|
-
|
|
726
|
+
emitAcceptanceStampFromPlan(projectRoot, vbrief.plan);
|
|
727
727
|
return ["created", target, `CREATED ${folder}/${filename}`];
|
|
728
728
|
}
|
|
729
|
-
function emitIntakeAcceptanceStamp(projectRoot, plan) {
|
|
730
|
-
const rec = plan !== null && typeof plan === "object" && !Array.isArray(plan)
|
|
731
|
-
? plan
|
|
732
|
-
: null;
|
|
733
|
-
if (rec === null) {
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
const acceptance = rec.acceptance !== null && typeof rec.acceptance === "object" && !Array.isArray(rec.acceptance)
|
|
737
|
-
? rec.acceptance
|
|
738
|
-
: null;
|
|
739
|
-
if (acceptance === null) {
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
const env = process.env;
|
|
743
|
-
const dest = env[ENV_RUN_SUMMARY_PATH];
|
|
744
|
-
if (dest === undefined || dest.trim().length === 0) {
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
try {
|
|
748
|
-
const sessionId = typeof env.DEFT_SESSION_ID === "string" && env.DEFT_SESSION_ID.trim().length > 0
|
|
749
|
-
? env.DEFT_SESSION_ID.trim()
|
|
750
|
-
: "issue-ingest";
|
|
751
|
-
const emitter = new RunSummaryEmitter({ projectRoot, sessionId, env });
|
|
752
|
-
const commands = Array.isArray(acceptance.commands) ? acceptance.commands.length : 0;
|
|
753
|
-
const clauses = Array.isArray(acceptance.clauses) ? acceptance.clauses.length : 0;
|
|
754
|
-
emitter.emitAcceptanceStamp({
|
|
755
|
-
rung: typeof acceptance.source_rung === "string" ? acceptance.source_rung : "project_floor",
|
|
756
|
-
none_stated: acceptance.none_stated === true,
|
|
757
|
-
command_count: commands,
|
|
758
|
-
clause_count: clauses,
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
catch {
|
|
762
|
-
// fail-open
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
729
|
export function ingestBulk(issues, options) {
|
|
766
730
|
let filtered = issues;
|
|
767
731
|
if (options.label !== undefined && options.label !== null) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ParentLineageResult } from "../scope/parent-lineage.js";
|
|
2
|
+
import { type FetchOriginUpdatedAt } from "../vbrief-reconcile/origin-freshness.js";
|
|
2
3
|
/** Canonical eligibility folder — only vbrief/active/ may spawn implementation. */
|
|
3
4
|
export declare const ACTIVE_FOLDER = "active";
|
|
4
5
|
/** Canonical eligibility status — only `running` signals an active handoff. */
|
|
@@ -20,6 +21,10 @@ export interface EvaluateOptions {
|
|
|
20
21
|
readonly projectRoot?: string;
|
|
21
22
|
/** Skip parent-lineage check (tests / opt-out). Default false. */
|
|
22
23
|
readonly skipParentLineage?: boolean;
|
|
24
|
+
/** Skip origin timestamp freshness (#3363). Default false. */
|
|
25
|
+
readonly skipOriginFreshness?: boolean;
|
|
26
|
+
/** Injected origin fetch for tests. Default: live `gh api` REST. */
|
|
27
|
+
readonly fetchOriginUpdatedAt?: FetchOriginUpdatedAt;
|
|
23
28
|
}
|
|
24
29
|
/** Substitute `{path}` without `$`-pattern expansion in user paths (#1721). */
|
|
25
30
|
export declare function formatActivateHint(path: string): string;
|
|
@@ -2,6 +2,7 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname } from "node:path";
|
|
3
3
|
import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
|
|
4
4
|
import { evaluateParentLineage, formatParentLineageLine, } from "../scope/parent-lineage.js";
|
|
5
|
+
import { evaluateOriginFreshness, } from "../vbrief-reconcile/origin-freshness.js";
|
|
5
6
|
/** Canonical eligibility folder — only vbrief/active/ may spawn implementation. */
|
|
6
7
|
export const ACTIVE_FOLDER = "active";
|
|
7
8
|
/** Canonical eligibility status — only `running` signals an active handoff. */
|
|
@@ -154,6 +155,19 @@ export function evaluate(vbriefPath, options = {}) {
|
|
|
154
155
|
message: buildReject(path, `${lineage.message}${defect}\n ${formatParentLineageLine(lineage)}`),
|
|
155
156
|
};
|
|
156
157
|
}
|
|
158
|
+
// #3363: fail closed when the live GitHub origin is newer than the brief.
|
|
159
|
+
const originFreshness = evaluateOriginFreshness(record, {
|
|
160
|
+
skip: options.skipOriginFreshness === true,
|
|
161
|
+
fetchOriginUpdatedAt: options.fetchOriginUpdatedAt,
|
|
162
|
+
cwd: options.projectRoot,
|
|
163
|
+
});
|
|
164
|
+
if (!originFreshness.ok) {
|
|
165
|
+
return {
|
|
166
|
+
exitCode: 1,
|
|
167
|
+
parentLineage: lineage,
|
|
168
|
+
message: buildReject(path, originFreshness.message),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
157
171
|
// Keep the historical OK line when lineage is N/A (backward-compatible tests / agents).
|
|
158
172
|
const message = lineage.applicable
|
|
159
173
|
? `OK ${path} -- ready for implementation. parent lineage OK ` +
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export type { EvaluateResult } from "./evaluate.js";
|
|
1
|
+
export type { EvaluateOptions, EvaluateResult } from "./evaluate.js";
|
|
2
2
|
export { ACTIVATE_HINT, ACTIVE_FOLDER, ELIGIBLE_LIFECYCLE_DIRS, ELIGIBLE_STATUS, emitJson, evaluate, formatActivateHint, PREFLIGHT_USAGE_HINT, } from "./evaluate.js";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { PlanAcceptance } from "./types.js";
|
|
8
8
|
export declare const EMPTY_AC_OUTCOME: "soft_empty";
|
|
9
9
|
export declare const EMPTY_AC_CAUSE = "no acceptance stamped \u2014 floor is empty in this project";
|
|
10
|
-
export declare const EMPTY_AC_REMEDY = "stamp plan.acceptance (
|
|
10
|
+
export declare const EMPTY_AC_REMEDY = "stamp plan.acceptance via clause derivation (scope:activate / scope:promote runs #3323, or task issue:ingest)";
|
|
11
11
|
export type VerifyAcResolution = "verified-pass" | "empty-pass" | "soft_empty" | "fail" | "config" | "skipped";
|
|
12
12
|
/** True when this project check composition includes a suite gate (#3188). */
|
|
13
13
|
export declare function projectHasSuiteFloor(projectRoot: string): boolean;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { isFrameworkRepoRoot } from "../check/context.js";
|
|
8
8
|
export const EMPTY_AC_OUTCOME = "soft_empty";
|
|
9
9
|
export const EMPTY_AC_CAUSE = "no acceptance stamped — floor is empty in this project";
|
|
10
|
-
export const EMPTY_AC_REMEDY = "stamp plan.acceptance (
|
|
10
|
+
export const EMPTY_AC_REMEDY = "stamp plan.acceptance via clause derivation (scope:activate / scope:promote runs #3323, or task issue:ingest)";
|
|
11
11
|
/** True when this project check composition includes a suite gate (#3188). */
|
|
12
12
|
export function projectHasSuiteFloor(projectRoot) {
|
|
13
13
|
return isFrameworkRepoRoot(projectRoot);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* floor, empty resolution is soft_empty — not a green run (#3334).
|
|
9
9
|
*/
|
|
10
10
|
import { type EvaluateLiteralAcceptanceOptions, type LiteralAcceptanceGateResult } from "../literal-acceptance/index.js";
|
|
11
|
+
import { type AcceptanceRunSummaryOutcome } from "../run-summary/index.js";
|
|
11
12
|
import { type ClauseWalkResult } from "../verify-ac/clauses.js";
|
|
12
13
|
import { type VerifyAcResolution } from "./empty-resolution.js";
|
|
13
14
|
import type { AcSourceRung, PlanAcceptance } from "./types.js";
|
|
@@ -62,15 +63,43 @@ export interface EvaluateVerifyAcOptions extends EvaluateLiteralAcceptanceOption
|
|
|
62
63
|
* emit after the #3285 bank checkpoint.
|
|
63
64
|
*/
|
|
64
65
|
readonly skipAcceptanceEmit?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Raw plan.acceptance as observed on the brief (#3355). Distinct from the
|
|
68
|
+
* synthesized floor: stamp only when the plan actually carries a block.
|
|
69
|
+
*/
|
|
70
|
+
readonly observedAcceptance?: unknown;
|
|
71
|
+
/**
|
|
72
|
+
* Active scope key for product-oracle check_id namespacing (#3337).
|
|
73
|
+
* Prefer plan.id; path stem when id is missing. Multi-active verify:ac
|
|
74
|
+
* under one session must not share a single global `verify:ac` check id.
|
|
75
|
+
*/
|
|
76
|
+
readonly oracleScopeKey?: string | null;
|
|
65
77
|
}
|
|
66
78
|
/**
|
|
67
79
|
* Evaluate product AC from an in-memory plan.
|
|
68
80
|
*/
|
|
69
81
|
export declare function evaluateVerifyAcFromPlan(plan: Record<string, unknown>, options?: EvaluateVerifyAcOptions): VerifyAcResult;
|
|
82
|
+
/**
|
|
83
|
+
* CLI-only early returns that never reach evaluate still emit an acceptance
|
|
84
|
+
* outcome so field streams see config-error / soft-missing (#3355).
|
|
85
|
+
*/
|
|
86
|
+
export declare function emitVerifyAcTerminalOutcome(input: {
|
|
87
|
+
readonly projectRoot: string;
|
|
88
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
89
|
+
readonly outcome: AcceptanceRunSummaryOutcome;
|
|
90
|
+
readonly sourceRung?: AcSourceRung;
|
|
91
|
+
readonly noneStated?: boolean;
|
|
92
|
+
}): void;
|
|
70
93
|
/**
|
|
71
94
|
* Evaluate from xBRIEF path.
|
|
72
95
|
*/
|
|
73
96
|
export declare function evaluateVerifyAcFromPath(xbriefPath: string, options?: EvaluateVerifyAcOptions): VerifyAcResult;
|
|
97
|
+
/**
|
|
98
|
+
* Unique product-oracle scope key for one active xBRIEF path (#3337).
|
|
99
|
+
* Relative path is always unique across active roots; plan.id alone is not
|
|
100
|
+
* (duplicate ids / same stem in xbrief+vbrief). Prefer `id@relPath` when both exist.
|
|
101
|
+
*/
|
|
102
|
+
export declare function resolveOracleScopeKey(plan: Record<string, unknown>, xbriefPath: string, projectRoot: string): string;
|
|
74
103
|
/** Pure: product AC is required at every ceremony depth (#3284 / #3267 / #3156). */
|
|
75
104
|
export declare function isVerifyAcRequiredAtCeremonyDepth(_depth: string | null | undefined): boolean;
|
|
76
105
|
//# sourceMappingURL=evaluate.d.ts.map
|