project-tiny-context-harness 0.8.7 → 0.8.10
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/README.md +146 -118
- package/assets/README.md +276 -246
- package/assets/README.zh-CN.md +169 -137
- package/assets/agents/AGENTS_CORE.md +8 -6
- package/assets/context_templates/architecture.md +10 -7
- package/assets/context_templates/verification.md +7 -5
- package/assets/skills/context_development_engineer/SKILL.md +37 -24
- package/assets/skills/design-resource-authoring/SKILL.md +12 -12
- package/assets/skills/design-resource-authoring/references/downstream-handoff.md +57 -48
- package/assets/skills/long-task-workflow/SKILL.md +8 -6
- package/assets/skills/long-task-workflow/references/authority-lifecycle.md +1 -1
- package/assets/skills/long-task-workflow/references/contract-authoring.md +13 -11
- package/dist/commands/check-modularity.js +16 -3
- package/dist/commands/design-resource.js +62 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/design-resource-fact-manifest-validation.d.ts +3 -2
- package/dist/lib/design-resource-fact-manifest-validation.js +23 -18
- package/dist/lib/design-resource-fact-universe-catalog.js +18 -8
- package/dist/lib/design-resource-fact-universe-helpers.js +9 -5
- package/dist/lib/design-resource-handoff-bundle.d.ts +31 -0
- package/dist/lib/design-resource-handoff-bundle.js +171 -0
- package/dist/lib/design-resource-handoff-file-validation.d.ts +1 -1
- package/dist/lib/design-resource-handoff-file-validation.js +1 -9
- package/dist/lib/design-resource-handoff-manifest-projection.d.ts +3 -0
- package/dist/lib/design-resource-handoff-manifest-projection.js +37 -0
- package/dist/lib/design-resource-handoff-parser.d.ts +7 -2
- package/dist/lib/design-resource-handoff-parser.js +32 -12
- package/dist/lib/design-resource-handoff-set-integrity.d.ts +8 -0
- package/dist/lib/design-resource-handoff-set-integrity.js +93 -0
- package/dist/lib/design-resource-handoff-shape.d.ts +2 -1
- package/dist/lib/design-resource-handoff-shape.js +62 -0
- package/dist/lib/design-resource-handoff-snapshot.d.ts +6 -0
- package/dist/lib/design-resource-handoff-snapshot.js +21 -0
- package/dist/lib/design-resource-handoff-types.d.ts +23 -1
- package/dist/lib/design-resource-handoff-validation.d.ts +2 -1
- package/dist/lib/design-resource-handoff-validation.js +45 -22
- package/dist/lib/long-task-design-resource-handoff.d.ts +15 -0
- package/dist/lib/long-task-design-resource-handoff.js +63 -173
- package/dist/lib/long-task-design-resource-method-binding.d.ts +8 -0
- package/dist/lib/long-task-design-resource-method-binding.js +135 -0
- package/dist/lib/long-task-source-item-parser.d.ts +8 -0
- package/dist/lib/long-task-source-item-parser.js +29 -107
- package/dist/lib/long-task-source-owned-sections.d.ts +29 -0
- package/dist/lib/long-task-source-owned-sections.js +90 -0
- package/dist/lib/migrations.js +2 -0
- package/dist/lib/modularity-capability-migration.d.ts +2 -0
- package/dist/lib/modularity-capability-migration.js +165 -0
- package/dist/lib/modularity-python.d.ts +7 -0
- package/dist/lib/modularity-python.js +191 -0
- package/dist/lib/modularity.d.ts +12 -5
- package/dist/lib/modularity.js +110 -44
- package/dist/lib/source-files.d.ts +2 -0
- package/dist/lib/source-files.js +21 -0
- package/dist/lib/source-line-scanner.d.ts +3 -0
- package/dist/lib/source-line-scanner.js +42 -0
- package/package.json +84 -84
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseDesignResourceHandoffInputShape } from "./design-resource-handoff-shape.js";
|
|
2
2
|
import { parseSemanticFactManifestShape } from "./semantic-fact-manifest-shape.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { assertNoOpenOwnedSection, assertOwnedSourceSectionsClosed, consumeBackgroundBoundary, consumeOwnedText, consumeSourceItemBoundary, } from "./long-task-source-owned-sections.js";
|
|
4
|
+
import { formalBlockBody, forEachSourceLine } from "./source-line-scanner.js";
|
|
5
|
+
import { parseStrictYaml } from "./strict-codec.js";
|
|
5
6
|
const DESIGN_RESOURCE_START = /^\s*```yaml[ \t]+design-resource-handoff-v1[ \t]*$/u;
|
|
6
7
|
const SEMANTIC_FACT_START = /^\s*```yaml[ \t]+semantic-fact-manifest-v1[ \t]*$/u;
|
|
7
8
|
const FORMAL_BLOCK_END = /^\s*```[ \t]*$/u;
|
|
8
9
|
export function parseSourceItems(sourcePath, content) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
return parseSourceDocument(sourcePath, content).items;
|
|
11
|
+
}
|
|
12
|
+
export function parseSourceDocument(sourcePath, content) {
|
|
13
|
+
const state = sourceParseState(sourcePath, content);
|
|
14
|
+
forEachSourceLine(content, (line, startOffset, _endOffset, nextOffset, lineNumber) => consumeSourceLine(state, line, lineNumber, startOffset, nextOffset));
|
|
13
15
|
assertSourceSectionsClosed(state);
|
|
14
|
-
return
|
|
16
|
+
return {
|
|
17
|
+
items: state.items,
|
|
18
|
+
designResourceHandoff: state.designResourceHandoff,
|
|
19
|
+
semanticFactManifest: state.semanticFactManifest,
|
|
20
|
+
};
|
|
15
21
|
}
|
|
16
|
-
function sourceParseState(sourcePath) {
|
|
22
|
+
function sourceParseState(sourcePath, content) {
|
|
17
23
|
return {
|
|
18
24
|
sourcePath,
|
|
25
|
+
content,
|
|
19
26
|
items: [],
|
|
20
27
|
seen: new Set(),
|
|
21
28
|
seenBackground: new Set(),
|
|
@@ -24,12 +31,14 @@ function sourceParseState(sourcePath) {
|
|
|
24
31
|
formalBlock: null,
|
|
25
32
|
designResourceBlocks: 0,
|
|
26
33
|
semanticFactBlocks: 0,
|
|
34
|
+
designResourceHandoff: null,
|
|
35
|
+
semanticFactManifest: null,
|
|
27
36
|
};
|
|
28
37
|
}
|
|
29
|
-
function consumeSourceLine(state, line, lineNumber) {
|
|
30
|
-
if (consumeOpenFormalBlock(state, line))
|
|
38
|
+
function consumeSourceLine(state, line, lineNumber, startOffset, nextOffset) {
|
|
39
|
+
if (consumeOpenFormalBlock(state, line, startOffset))
|
|
31
40
|
return;
|
|
32
|
-
if (startFormalBlock(state, line, lineNumber))
|
|
41
|
+
if (startFormalBlock(state, line, lineNumber, nextOffset))
|
|
33
42
|
return;
|
|
34
43
|
if (consumeSourceItemBoundary(state, line, lineNumber))
|
|
35
44
|
return;
|
|
@@ -37,19 +46,17 @@ function consumeSourceLine(state, line, lineNumber) {
|
|
|
37
46
|
return;
|
|
38
47
|
consumeOwnedText(state, line, lineNumber);
|
|
39
48
|
}
|
|
40
|
-
function consumeOpenFormalBlock(state, line) {
|
|
49
|
+
function consumeOpenFormalBlock(state, line, startOffset) {
|
|
41
50
|
if (!state.formalBlock)
|
|
42
51
|
return false;
|
|
43
|
-
if (!FORMAL_BLOCK_END.test(line))
|
|
44
|
-
state.formalBlock.lines.push(line);
|
|
52
|
+
if (!FORMAL_BLOCK_END.test(line))
|
|
45
53
|
return true;
|
|
46
|
-
}
|
|
47
54
|
try {
|
|
48
|
-
const value = parseStrictYaml(state.formalBlock.
|
|
55
|
+
const value = parseStrictYaml(formalBlockBody(state.content, state.formalBlock.bodyStartOffset, startOffset, state.formalBlock.owner === "design-resource-handoff-v1"));
|
|
49
56
|
if (state.formalBlock.owner === "design-resource-handoff-v1")
|
|
50
|
-
|
|
57
|
+
state.designResourceHandoff = parseDesignResourceHandoffInputShape(value);
|
|
51
58
|
else
|
|
52
|
-
parseSemanticFactManifestShape(value);
|
|
59
|
+
state.semanticFactManifest = parseSemanticFactManifestShape(value);
|
|
53
60
|
}
|
|
54
61
|
catch (error) {
|
|
55
62
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -58,7 +65,7 @@ function consumeOpenFormalBlock(state, line) {
|
|
|
58
65
|
state.formalBlock = null;
|
|
59
66
|
return true;
|
|
60
67
|
}
|
|
61
|
-
function startFormalBlock(state, line, lineNumber) {
|
|
68
|
+
function startFormalBlock(state, line, lineNumber, nextOffset) {
|
|
62
69
|
const owner = DESIGN_RESOURCE_START.test(line)
|
|
63
70
|
? "design-resource-handoff-v1"
|
|
64
71
|
: SEMANTIC_FACT_START.test(line)
|
|
@@ -79,98 +86,13 @@ function startFormalBlock(state, line, lineNumber) {
|
|
|
79
86
|
}
|
|
80
87
|
state.formalBlock = {
|
|
81
88
|
owner,
|
|
82
|
-
|
|
89
|
+
bodyStartOffset: nextOffset,
|
|
83
90
|
line: lineNumber,
|
|
84
91
|
};
|
|
85
92
|
return true;
|
|
86
93
|
}
|
|
87
|
-
function consumeSourceItemBoundary(state, line, lineNumber) {
|
|
88
|
-
const start = SOURCE_ITEM_START.exec(line);
|
|
89
|
-
if (start) {
|
|
90
|
-
assertNoOpenOwnedSection(state, lineNumber);
|
|
91
|
-
state.open = {
|
|
92
|
-
...parseSourceItemStartMarker(state.sourcePath, lineNumber, start[1]),
|
|
93
|
-
lines: [],
|
|
94
|
-
line: lineNumber,
|
|
95
|
-
};
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
if (!SOURCE_ITEM_END.test(line))
|
|
99
|
-
return false;
|
|
100
|
-
if (!state.open)
|
|
101
|
-
throw new Error(`source_item_end_without_start:${state.sourcePath}:${lineNumber}`);
|
|
102
|
-
closeSourceItem(state);
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
function closeSourceItem(state) {
|
|
106
|
-
const open = state.open;
|
|
107
|
-
const normalizedText = normalizeSourceItemText(open.lines.join("\n"));
|
|
108
|
-
if (!normalizedText)
|
|
109
|
-
throw new Error(`source_item_empty:${state.sourcePath}:${open.key}`);
|
|
110
|
-
if (state.seen.has(open.key))
|
|
111
|
-
throw new Error(`source_item_key_duplicate:${open.key}`);
|
|
112
|
-
state.seen.add(open.key);
|
|
113
|
-
state.items.push({
|
|
114
|
-
key: open.key,
|
|
115
|
-
kind: open.kind,
|
|
116
|
-
...(open.aspect ? { aspect: open.aspect } : {}),
|
|
117
|
-
source_path: state.sourcePath,
|
|
118
|
-
normalized_text: normalizedText,
|
|
119
|
-
text_sha256: sha256Hex(normalizedText),
|
|
120
|
-
...(open.risk_semantics ? { risk_semantics: open.risk_semantics } : {}),
|
|
121
|
-
});
|
|
122
|
-
state.open = null;
|
|
123
|
-
}
|
|
124
|
-
function consumeBackgroundBoundary(state, line, lineNumber) {
|
|
125
|
-
const start = SOURCE_BACKGROUND_START.exec(line);
|
|
126
|
-
if (start) {
|
|
127
|
-
assertNoOpenOwnedSection(state, lineNumber);
|
|
128
|
-
const declaration = parseSourceBackgroundMarker(state.sourcePath, lineNumber, start[1]);
|
|
129
|
-
if (state.seenBackground.has(declaration.key))
|
|
130
|
-
throw new Error(`source_background_key_duplicate:${declaration.key}`);
|
|
131
|
-
state.seenBackground.add(declaration.key);
|
|
132
|
-
state.background = {
|
|
133
|
-
...declaration,
|
|
134
|
-
substantiveLines: 0,
|
|
135
|
-
line: lineNumber,
|
|
136
|
-
};
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
if (!SOURCE_BACKGROUND_END.test(line))
|
|
140
|
-
return false;
|
|
141
|
-
if (!state.background)
|
|
142
|
-
throw new Error(`source_background_end_without_start:${state.sourcePath}:${lineNumber}`);
|
|
143
|
-
if (!state.background.substantiveLines)
|
|
144
|
-
throw new Error(`source_background_empty:${state.sourcePath}:${state.background.key}`);
|
|
145
|
-
state.background = null;
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
function assertNoOpenOwnedSection(state, lineNumber) {
|
|
149
|
-
if (!state.open && !state.background)
|
|
150
|
-
return;
|
|
151
|
-
throw new Error(`source_section_nested_or_overlapping:${state.sourcePath}:${state.open?.key ?? state.background?.key}:${lineNumber}`);
|
|
152
|
-
}
|
|
153
|
-
function consumeOwnedText(state, line, lineNumber) {
|
|
154
|
-
if (line.includes("ty-source-item:start") ||
|
|
155
|
-
line.includes("ty-source-item:end") ||
|
|
156
|
-
line.includes("ty-source-background:start") ||
|
|
157
|
-
line.includes("ty-source-background:end"))
|
|
158
|
-
throw new Error(`source_item_marker_invalid:${state.sourcePath}:${lineNumber}`);
|
|
159
|
-
if (state.open)
|
|
160
|
-
state.open.lines.push(line);
|
|
161
|
-
else if (state.background) {
|
|
162
|
-
validateSourceBackgroundLine(state.sourcePath, state.background.key, state.background.reason, lineNumber, line);
|
|
163
|
-
if (line.trim())
|
|
164
|
-
state.background.substantiveLines += 1;
|
|
165
|
-
}
|
|
166
|
-
else if (line.trim())
|
|
167
|
-
throw new Error(`source_text_unclassified:${state.sourcePath}:${lineNumber}`);
|
|
168
|
-
}
|
|
169
94
|
function assertSourceSectionsClosed(state) {
|
|
170
|
-
|
|
171
|
-
throw new Error(`source_item_unclosed:${state.sourcePath}:${state.open.key}:${state.open.line}`);
|
|
172
|
-
if (state.background)
|
|
173
|
-
throw new Error(`source_background_unclosed:${state.sourcePath}:${state.background.key}:${state.background.line}`);
|
|
95
|
+
assertOwnedSourceSectionsClosed(state);
|
|
174
96
|
if (state.formalBlock)
|
|
175
97
|
throw new Error(`source_formal_block_unclosed:${state.sourcePath}:${state.formalBlock.owner}:${state.formalBlock.line}`);
|
|
176
98
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CompiledSourceItemV2, SourceItemKind } from "./long-task-delivery-types.js";
|
|
2
|
+
interface OpenSourceItem {
|
|
3
|
+
key: string;
|
|
4
|
+
kind: SourceItemKind;
|
|
5
|
+
aspect?: CompiledSourceItemV2["aspect"];
|
|
6
|
+
risk_semantics?: CompiledSourceItemV2["risk_semantics"];
|
|
7
|
+
lines: string[];
|
|
8
|
+
line: number;
|
|
9
|
+
}
|
|
10
|
+
interface OpenSourceBackground {
|
|
11
|
+
key: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
substantiveLines: number;
|
|
14
|
+
line: number;
|
|
15
|
+
}
|
|
16
|
+
export interface OwnedSourceParseState {
|
|
17
|
+
sourcePath: string;
|
|
18
|
+
items: CompiledSourceItemV2[];
|
|
19
|
+
seen: Set<string>;
|
|
20
|
+
seenBackground: Set<string>;
|
|
21
|
+
open: OpenSourceItem | null;
|
|
22
|
+
background: OpenSourceBackground | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function consumeSourceItemBoundary(state: OwnedSourceParseState, line: string, lineNumber: number): boolean;
|
|
25
|
+
export declare function consumeBackgroundBoundary(state: OwnedSourceParseState, line: string, lineNumber: number): boolean;
|
|
26
|
+
export declare function assertNoOpenOwnedSection(state: OwnedSourceParseState, lineNumber: number): void;
|
|
27
|
+
export declare function consumeOwnedText(state: OwnedSourceParseState, line: string, lineNumber: number): void;
|
|
28
|
+
export declare function assertOwnedSourceSectionsClosed(state: OwnedSourceParseState): void;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { normalizeSourceItemText, parseSourceBackgroundMarker, parseSourceItemStartMarker, SOURCE_BACKGROUND_END, SOURCE_BACKGROUND_START, SOURCE_ITEM_END, SOURCE_ITEM_START, validateSourceBackgroundLine, } from "./long-task-source-markers.js";
|
|
2
|
+
import { sha256Hex } from "./strict-codec.js";
|
|
3
|
+
export function consumeSourceItemBoundary(state, line, lineNumber) {
|
|
4
|
+
const start = SOURCE_ITEM_START.exec(line);
|
|
5
|
+
if (start) {
|
|
6
|
+
assertNoOpenOwnedSection(state, lineNumber);
|
|
7
|
+
state.open = {
|
|
8
|
+
...parseSourceItemStartMarker(state.sourcePath, lineNumber, start[1]),
|
|
9
|
+
lines: [],
|
|
10
|
+
line: lineNumber,
|
|
11
|
+
};
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (!SOURCE_ITEM_END.test(line))
|
|
15
|
+
return false;
|
|
16
|
+
if (!state.open)
|
|
17
|
+
throw new Error(`source_item_end_without_start:${state.sourcePath}:${lineNumber}`);
|
|
18
|
+
closeSourceItem(state);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
function closeSourceItem(state) {
|
|
22
|
+
const open = state.open;
|
|
23
|
+
const normalizedText = normalizeSourceItemText(open.lines.join("\n"));
|
|
24
|
+
if (!normalizedText)
|
|
25
|
+
throw new Error(`source_item_empty:${state.sourcePath}:${open.key}`);
|
|
26
|
+
if (state.seen.has(open.key))
|
|
27
|
+
throw new Error(`source_item_key_duplicate:${open.key}`);
|
|
28
|
+
state.seen.add(open.key);
|
|
29
|
+
state.items.push({
|
|
30
|
+
key: open.key,
|
|
31
|
+
kind: open.kind,
|
|
32
|
+
...(open.aspect ? { aspect: open.aspect } : {}),
|
|
33
|
+
source_path: state.sourcePath,
|
|
34
|
+
normalized_text: normalizedText,
|
|
35
|
+
text_sha256: sha256Hex(normalizedText),
|
|
36
|
+
...(open.risk_semantics ? { risk_semantics: open.risk_semantics } : {}),
|
|
37
|
+
});
|
|
38
|
+
state.open = null;
|
|
39
|
+
}
|
|
40
|
+
export function consumeBackgroundBoundary(state, line, lineNumber) {
|
|
41
|
+
const start = SOURCE_BACKGROUND_START.exec(line);
|
|
42
|
+
if (start) {
|
|
43
|
+
assertNoOpenOwnedSection(state, lineNumber);
|
|
44
|
+
const declaration = parseSourceBackgroundMarker(state.sourcePath, lineNumber, start[1]);
|
|
45
|
+
if (state.seenBackground.has(declaration.key))
|
|
46
|
+
throw new Error(`source_background_key_duplicate:${declaration.key}`);
|
|
47
|
+
state.seenBackground.add(declaration.key);
|
|
48
|
+
state.background = {
|
|
49
|
+
...declaration,
|
|
50
|
+
substantiveLines: 0,
|
|
51
|
+
line: lineNumber,
|
|
52
|
+
};
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (!SOURCE_BACKGROUND_END.test(line))
|
|
56
|
+
return false;
|
|
57
|
+
if (!state.background)
|
|
58
|
+
throw new Error(`source_background_end_without_start:${state.sourcePath}:${lineNumber}`);
|
|
59
|
+
if (!state.background.substantiveLines)
|
|
60
|
+
throw new Error(`source_background_empty:${state.sourcePath}:${state.background.key}`);
|
|
61
|
+
state.background = null;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
export function assertNoOpenOwnedSection(state, lineNumber) {
|
|
65
|
+
if (!state.open && !state.background)
|
|
66
|
+
return;
|
|
67
|
+
throw new Error(`source_section_nested_or_overlapping:${state.sourcePath}:${state.open?.key ?? state.background?.key}:${lineNumber}`);
|
|
68
|
+
}
|
|
69
|
+
export function consumeOwnedText(state, line, lineNumber) {
|
|
70
|
+
if (line.includes("ty-source-item:start") ||
|
|
71
|
+
line.includes("ty-source-item:end") ||
|
|
72
|
+
line.includes("ty-source-background:start") ||
|
|
73
|
+
line.includes("ty-source-background:end"))
|
|
74
|
+
throw new Error(`source_item_marker_invalid:${state.sourcePath}:${lineNumber}`);
|
|
75
|
+
if (state.open)
|
|
76
|
+
state.open.lines.push(line);
|
|
77
|
+
else if (state.background) {
|
|
78
|
+
validateSourceBackgroundLine(state.sourcePath, state.background.key, state.background.reason, lineNumber, line);
|
|
79
|
+
if (line.trim())
|
|
80
|
+
state.background.substantiveLines += 1;
|
|
81
|
+
}
|
|
82
|
+
else if (line.trim())
|
|
83
|
+
throw new Error(`source_text_unclassified:${state.sourcePath}:${lineNumber}`);
|
|
84
|
+
}
|
|
85
|
+
export function assertOwnedSourceSectionsClosed(state) {
|
|
86
|
+
if (state.open)
|
|
87
|
+
throw new Error(`source_item_unclosed:${state.sourcePath}:${state.open.key}:${state.open.line}`);
|
|
88
|
+
if (state.background)
|
|
89
|
+
throw new Error(`source_background_unclosed:${state.sourcePath}:${state.background.key}:${state.background.line}`);
|
|
90
|
+
}
|
package/dist/lib/migrations.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createDesignMdIfMissing, DESIGN_MD_PATH } from "./design-md.js";
|
|
|
9
9
|
import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged, } from "./fs.js";
|
|
10
10
|
import { harnessConfigPath, harnessRoot } from "./harness-root.js";
|
|
11
11
|
import { legacySdlcHarnessMigration } from "./legacy-sdlc-migration.js";
|
|
12
|
+
import { modularityCapabilityWaiverMigration } from "./modularity-capability-migration.js";
|
|
12
13
|
import { parseYaml, stringifyYaml } from "./yaml.js";
|
|
13
14
|
import { semanticDriftMigrationFields } from "./long-task-semantic-drift-migration.js";
|
|
14
15
|
async function verifyNoop() {
|
|
@@ -18,6 +19,7 @@ const REMOVED_NORMAL_LONG_TASK_SKILL = "normal-long-task";
|
|
|
18
19
|
const REMOVED_NORMAL_LONG_TASK_SKILL_SHA256 = "7a4f755da9a32e07f258d6c7fc5e5c70395257c72efb8a9ad4995cddcc4c73ba";
|
|
19
20
|
export const migrations = [
|
|
20
21
|
legacySdlcHarnessMigration,
|
|
22
|
+
modularityCapabilityWaiverMigration,
|
|
21
23
|
{
|
|
22
24
|
id: "long-task-v2-semantic-drift-authority",
|
|
23
25
|
introducedIn: "0.7.2",
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
4
|
+
import { harnessConfigPath } from "./harness-root.js";
|
|
5
|
+
import { DEFAULT_MODULARITY_LINE_LIMIT, hasLegacyHeuristicOnlyWaiverRisk, isLifecycleCompleteModularityWaiverConfig, isLikelyGeneratedSourceContent, } from "./modularity.js";
|
|
6
|
+
import { toPosix } from "./source-files.js";
|
|
7
|
+
import { parseYaml, stringifyYaml } from "./yaml.js";
|
|
8
|
+
const MIGRATION_ID = "modularity-capability-waiver-cleanup";
|
|
9
|
+
const INTRODUCED_IN = "0.8.8";
|
|
10
|
+
const DESCRIPTION = "Remove scoped modularity waivers that existed only for unsupported cross-language heuristic metrics.";
|
|
11
|
+
const SCOPE = "<harnessRoot>/config.yaml modularity.waivers";
|
|
12
|
+
export const modularityCapabilityWaiverMigration = {
|
|
13
|
+
id: MIGRATION_ID,
|
|
14
|
+
introducedIn: INTRODUCED_IN,
|
|
15
|
+
description: DESCRIPTION,
|
|
16
|
+
scope: SCOPE,
|
|
17
|
+
risk: "safe",
|
|
18
|
+
manualMessage: "Remove only the waiver identified as legacy-heuristic-only; retain any waiver that still covers physical-line or supported language metrics.",
|
|
19
|
+
detect: detectModularityCapabilityWaivers,
|
|
20
|
+
apply: migrateModularityCapabilityWaivers,
|
|
21
|
+
verify: verifyModularityCapabilityWaivers,
|
|
22
|
+
};
|
|
23
|
+
async function detectModularityCapabilityWaivers(projectRoot) {
|
|
24
|
+
const config = await readRawConfig(projectRoot);
|
|
25
|
+
if (!config) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const candidates = await obsoleteWaiverIndexes(projectRoot, config.value);
|
|
29
|
+
return candidates.map(({ index, relativePath }) => ({
|
|
30
|
+
id: MIGRATION_ID,
|
|
31
|
+
introducedIn: INTRODUCED_IN,
|
|
32
|
+
description: DESCRIPTION,
|
|
33
|
+
scope: SCOPE,
|
|
34
|
+
status: "safe_pending",
|
|
35
|
+
path: `${config.relativePath}#modularity.waivers[${index}]`,
|
|
36
|
+
message: `${relativePath} exceeded only metrics from the retired cross-language JS heuristic; upgrade can remove this now-inapplicable waiver without changing supported line-risk coverage.`,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
async function migrateModularityCapabilityWaivers(projectRoot, _root, report) {
|
|
40
|
+
const config = await readRawConfig(projectRoot);
|
|
41
|
+
if (!config) {
|
|
42
|
+
report.skipped.push(MIGRATION_ID);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const candidates = await obsoleteWaiverIndexes(projectRoot, config.value);
|
|
46
|
+
if (candidates.length === 0) {
|
|
47
|
+
report.skipped.push(MIGRATION_ID);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const modularity = row(config.value.modularity);
|
|
51
|
+
const waivers = Array.isArray(modularity?.waivers) ? modularity.waivers : [];
|
|
52
|
+
const removed = new Set(candidates.map((candidate) => candidate.index));
|
|
53
|
+
const retained = waivers.filter((_waiver, index) => !removed.has(index));
|
|
54
|
+
if (retained.length > 0) {
|
|
55
|
+
modularity.waivers = retained;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
delete modularity.waivers;
|
|
59
|
+
}
|
|
60
|
+
if (await writeTextIfChanged(config.absolutePath, stringifyYaml(config.value))) {
|
|
61
|
+
report.changed.push(`${config.relativePath}#modularity.waivers removed=${removed.size}`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
report.skipped.push(MIGRATION_ID);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function verifyModularityCapabilityWaivers(projectRoot) {
|
|
68
|
+
const remaining = await detectModularityCapabilityWaivers(projectRoot);
|
|
69
|
+
if (remaining.length > 0) {
|
|
70
|
+
throw new Error("modularity capability waiver cleanup migration verification failed");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function obsoleteWaiverIndexes(projectRoot, config) {
|
|
74
|
+
const modularity = row(config.modularity);
|
|
75
|
+
if (!modularity ||
|
|
76
|
+
(modularity.policy ?? "scoped_waivers") !== "scoped_waivers") {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
if (modularity.limit !== undefined &&
|
|
80
|
+
(typeof modularity.limit !== "number" ||
|
|
81
|
+
!Number.isInteger(modularity.limit) ||
|
|
82
|
+
modularity.limit <= 0)) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const lineLimit = typeof modularity.limit === "number"
|
|
86
|
+
? modularity.limit
|
|
87
|
+
: DEFAULT_MODULARITY_LINE_LIMIT;
|
|
88
|
+
if (!Array.isArray(modularity.waivers)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const targets = modularity.waivers.map((value) => {
|
|
92
|
+
const waiver = row(value);
|
|
93
|
+
return safeProjectTarget(projectRoot, waiver?.path);
|
|
94
|
+
});
|
|
95
|
+
const targetCounts = new Map();
|
|
96
|
+
for (const target of targets) {
|
|
97
|
+
if (target) {
|
|
98
|
+
const identity = targetIdentity(target.relativePath);
|
|
99
|
+
targetCounts.set(identity, (targetCounts.get(identity) ?? 0) + 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const candidates = [];
|
|
103
|
+
for (const [index, value] of modularity.waivers.entries()) {
|
|
104
|
+
if (!isLifecycleCompleteModularityWaiverConfig(value)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const target = targets[index];
|
|
108
|
+
if (!target ||
|
|
109
|
+
targetCounts.get(targetIdentity(target.relativePath)) !== 1) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
let content;
|
|
113
|
+
try {
|
|
114
|
+
const stat = await fs.stat(target.absolutePath);
|
|
115
|
+
if (!stat.isFile()) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
content = await fs.readFile(target.absolutePath, "utf8");
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (isLikelyGeneratedSourceContent(content)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (hasLegacyHeuristicOnlyWaiverRisk(content, target.relativePath, lineLimit)) {
|
|
127
|
+
candidates.push({ index, relativePath: target.relativePath });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return candidates;
|
|
131
|
+
}
|
|
132
|
+
async function readRawConfig(projectRoot) {
|
|
133
|
+
const relativePath = await harnessConfigPath(projectRoot);
|
|
134
|
+
const absolutePath = path.join(projectRoot, relativePath);
|
|
135
|
+
if (!(await pathExists(absolutePath))) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const value = row(parseYaml(await readText(absolutePath)));
|
|
139
|
+
return value
|
|
140
|
+
? { absolutePath, relativePath: toPosix(relativePath), value }
|
|
141
|
+
: undefined;
|
|
142
|
+
}
|
|
143
|
+
function safeProjectTarget(projectRoot, value) {
|
|
144
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const absolutePath = path.resolve(projectRoot, value);
|
|
148
|
+
const relativePath = toPosix(path.relative(projectRoot, absolutePath));
|
|
149
|
+
if (relativePath === ".." ||
|
|
150
|
+
relativePath.startsWith("../") ||
|
|
151
|
+
path.isAbsolute(relativePath)) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
return { absolutePath, relativePath };
|
|
155
|
+
}
|
|
156
|
+
function row(value) {
|
|
157
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
158
|
+
? value
|
|
159
|
+
: undefined;
|
|
160
|
+
}
|
|
161
|
+
function targetIdentity(relativePath) {
|
|
162
|
+
return process.platform === "win32"
|
|
163
|
+
? relativePath.toLowerCase()
|
|
164
|
+
: relativePath;
|
|
165
|
+
}
|