project-tiny-context-harness 0.2.81 → 0.2.83
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 +11 -3
- package/assets/README.md +11 -3
- package/assets/README.zh-CN.md +9 -1
- package/assets/protected-harness-baseline.json +18 -0
- package/assets/skills/composite-long-task-workflow/SKILL.md +21 -1
- package/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md +10 -0
- package/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md +6 -15
- package/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md +32 -15
- package/dist/commands/composite-long-task.js +45 -2
- package/dist/lib/superpowers-task-ac010.d.ts +6 -0
- package/dist/lib/superpowers-task-ac010.js +26 -0
- package/dist/lib/superpowers-task-assertion-normalizers.js +8 -1
- package/dist/lib/superpowers-task-assertions.js +35 -20
- package/dist/lib/superpowers-task-attempt.d.ts +4 -0
- package/dist/lib/superpowers-task-attempt.js +102 -0
- package/dist/lib/superpowers-task-command-specs.d.ts +3 -0
- package/dist/lib/superpowers-task-command-specs.js +52 -0
- package/dist/lib/superpowers-task-compile.d.ts +4 -1
- package/dist/lib/superpowers-task-compile.js +7 -1
- package/dist/lib/superpowers-task-contradictions.d.ts +6 -0
- package/dist/lib/superpowers-task-contradictions.js +126 -0
- package/dist/lib/superpowers-task-current-evidence.d.ts +3 -0
- package/dist/lib/superpowers-task-current-evidence.js +154 -0
- package/dist/lib/superpowers-task-delivery.js +30 -18
- package/dist/lib/superpowers-task-derive.d.ts +1 -0
- package/dist/lib/superpowers-task-derive.js +76 -1
- package/dist/lib/superpowers-task-evidence-kernel.d.ts +12 -0
- package/dist/lib/superpowers-task-evidence-kernel.js +351 -0
- package/dist/lib/superpowers-task-evidence-records.d.ts +2 -0
- package/dist/lib/superpowers-task-evidence-records.js +55 -0
- package/dist/lib/superpowers-task-evidence.d.ts +10 -0
- package/dist/lib/superpowers-task-evidence.js +141 -0
- package/dist/lib/superpowers-task-fields.d.ts +22 -0
- package/dist/lib/superpowers-task-fields.js +276 -0
- package/dist/lib/superpowers-task-gates.js +36 -24
- package/dist/lib/superpowers-task-harness-drift.d.ts +11 -0
- package/dist/lib/superpowers-task-harness-drift.js +86 -0
- package/dist/lib/superpowers-task-protected-baseline.d.ts +10 -0
- package/dist/lib/superpowers-task-protected-baseline.js +47 -0
- package/dist/lib/superpowers-task-source-compile.js +93 -95
- package/dist/lib/superpowers-task-source-parser.js +1 -3
- package/dist/lib/superpowers-task-state-schema.d.ts +158 -3
- package/dist/lib/superpowers-task-state-schema.js +23 -1
- package/dist/lib/superpowers-task-state-shape.d.ts +3 -0
- package/dist/lib/superpowers-task-state-shape.js +50 -0
- package/dist/lib/superpowers-task-state.js +19 -39
- package/dist/lib/superpowers-task-status.js +11 -1
- package/dist/lib/superpowers-task-under-specified.d.ts +7 -0
- package/dist/lib/superpowers-task-under-specified.js +61 -0
- package/dist/lib/superpowers-task-validator.js +11 -37
- package/package.json +69 -69
- package/source-mappings.yaml +3 -0
|
@@ -1,16 +1,7 @@
|
|
|
1
|
+
import { evaluateCurrentAttemptEvidence } from "./superpowers-task-current-evidence.js";
|
|
2
|
+
import { MACHINE_VERIFIABLE_LAYER_NAMES, normalizeProofLayerId, normalizeProofLayerName } from "./superpowers-task-fields.js";
|
|
1
3
|
export { normalizeAssertionResult, normalizeNegativeEvidenceScan } from "./superpowers-task-assertion-normalizers.js";
|
|
2
|
-
export const MACHINE_VERIFIABLE_PROOF_LAYERS = new Set(
|
|
3
|
-
"ui_browser",
|
|
4
|
-
"api_schema",
|
|
5
|
-
"runtime",
|
|
6
|
-
"worker_runtime",
|
|
7
|
-
"data_artifact",
|
|
8
|
-
"integration",
|
|
9
|
-
"security_redaction",
|
|
10
|
-
"test",
|
|
11
|
-
"all_provider_all_runner",
|
|
12
|
-
"cleanup_stale_scan"
|
|
13
|
-
]);
|
|
4
|
+
export const MACHINE_VERIFIABLE_PROOF_LAYERS = new Set(MACHINE_VERIFIABLE_LAYER_NAMES);
|
|
14
5
|
export const UI_BROWSER_ASSERTION_TYPES = new Set(["browser_assertion", "playwright_assertion", "ui_browser_assertion"]);
|
|
15
6
|
const DEFAULT_UI_FORBIDDEN_FINAL_STATES = ["未验证", "不可用", "暂不可用", "页面无明显变化"];
|
|
16
7
|
const GENERIC_INVALID_EVIDENCE_TYPE_PATTERNS = [
|
|
@@ -33,7 +24,8 @@ const GENERIC_INVALID_EVIDENCE_TYPE_PATTERNS = [
|
|
|
33
24
|
/\bsample[-_ ]?for[-_ ]?full[-_ ]?population\b/i
|
|
34
25
|
];
|
|
35
26
|
export function proofLayerName(layerId) {
|
|
36
|
-
|
|
27
|
+
const raw = layerId.includes(".") ? layerId.slice(layerId.lastIndexOf(".") + 1) : layerId;
|
|
28
|
+
return normalizeProofLayerName(raw);
|
|
37
29
|
}
|
|
38
30
|
export function proofLayerAcId(layerId) {
|
|
39
31
|
return layerId.includes(".") ? layerId.slice(0, layerId.lastIndexOf(".")) : "";
|
|
@@ -76,6 +68,13 @@ export function evaluateProofLayerAssertions(state, layerId) {
|
|
|
76
68
|
if (!isMachineVerifiableLayer(layerId)) {
|
|
77
69
|
return { assertion_status: "not_applicable", blocking_assertion_failures: [], negative_evidence_findings: [] };
|
|
78
70
|
}
|
|
71
|
+
if (!state.current_attempt_id) {
|
|
72
|
+
return {
|
|
73
|
+
assertion_status: "missing",
|
|
74
|
+
blocking_assertion_failures: [`proof layer ${layerId} missing current attempt; current attempt is required for machine-backed completion`],
|
|
75
|
+
negative_evidence_findings: []
|
|
76
|
+
};
|
|
77
|
+
}
|
|
79
78
|
const layer = state.graph.proof_layers[layerId];
|
|
80
79
|
const evidenceById = new Map((state.evidence ?? []).map((item) => [item.evidence_id, item]));
|
|
81
80
|
const evidenceRecords = (layer?.evidence_ids ?? []).map((id) => evidenceById.get(id)).filter((item) => Boolean(item));
|
|
@@ -89,6 +88,7 @@ export function evaluateProofLayerAssertions(state, layerId) {
|
|
|
89
88
|
const blocking = [];
|
|
90
89
|
const negative = [];
|
|
91
90
|
for (const evidence of evidenceRecords) {
|
|
91
|
+
blocking.push(...evaluateCurrentAttemptEvidence(state, evidence, layerId));
|
|
92
92
|
blocking.push(...evaluateAssertionEvidence(evidence, layerId));
|
|
93
93
|
negative.push(...evaluateNegativeEvidence(evidence, layerId));
|
|
94
94
|
}
|
|
@@ -124,8 +124,8 @@ export function evaluateAssertionEvidence(evidence, layerId) {
|
|
|
124
124
|
failures.push(`${label} missing assertion result; ${layerName} proof not machine-backed`);
|
|
125
125
|
return failures;
|
|
126
126
|
}
|
|
127
|
-
if (assertion.schema_version !== "assertion-result-v1") {
|
|
128
|
-
failures.push(`${label} assertion_result.schema_version must be assertion-result-v1`);
|
|
127
|
+
if (assertion.schema_version !== "assertion-result-v1" && assertion.schema_version !== "assertion-result-v2") {
|
|
128
|
+
failures.push(`${label} assertion_result.schema_version must be assertion-result-v1 or assertion-result-v2`);
|
|
129
129
|
}
|
|
130
130
|
if (assertion.status !== "passed") {
|
|
131
131
|
failures.push(`${label} assertion_result.status=${assertion.status}; expected passed`);
|
|
@@ -139,11 +139,18 @@ export function evaluateAssertionEvidence(evidence, layerId) {
|
|
|
139
139
|
if (!assertion.target_ac_ids.includes(acId)) {
|
|
140
140
|
failures.push(`${label} assertion target ACs ${assertion.target_ac_ids.join(", ") || "(none)"} do not include ${acId}`);
|
|
141
141
|
}
|
|
142
|
-
|
|
142
|
+
const assertionTargetLayers = assertion.target_proof_layers.map(normalizeProofLayerId);
|
|
143
|
+
const normalizedLayerId = normalizeProofLayerId(layerId);
|
|
144
|
+
if (!assertionTargetLayers.includes(normalizedLayerId) && !assertionTargetLayers.includes(layerName)) {
|
|
143
145
|
failures.push(`${label} assertion target_proof_layers ${assertion.target_proof_layers.join(", ") || "(none)"} do not include ${layerId} or ${layerName}`);
|
|
144
146
|
}
|
|
145
147
|
failures.push(...checkAssertions(`${label} positive assertion`, assertion.positive_assertions));
|
|
146
148
|
failures.push(...checkAssertions(`${label} negative assertion`, assertion.negative_assertions));
|
|
149
|
+
failures.push(...checkAssertions(`${label} invalid completion signal`, assertion.invalid_completion_signals ?? []));
|
|
150
|
+
const missingRequiredTests = (assertion.required_test_ids ?? []).filter((testId) => !testId);
|
|
151
|
+
if (missingRequiredTests.length > 0) {
|
|
152
|
+
failures.push(`${label} assertion_result.required_test_ids contains empty test ids`);
|
|
153
|
+
}
|
|
147
154
|
if ((assertion.artifacts?.length ?? 0) === 0 && evidence.artifact_paths.length === 0) {
|
|
148
155
|
failures.push(`${label} assertion-backed evidence must include artifacts`);
|
|
149
156
|
}
|
|
@@ -189,9 +196,9 @@ function checkAssertions(prefix, checks) {
|
|
|
189
196
|
.map((check) => `${prefix} ${check.id || "(unnamed)"} status=${check.status}; expected passed${check.forbidden_text ? ` forbidden_text=${check.forbidden_text}` : ""}`);
|
|
190
197
|
}
|
|
191
198
|
export function evaluateNegativeEvidence(evidence, layerId) {
|
|
192
|
-
const scan = evidence.negative_evidence_scan;
|
|
199
|
+
const scan = evidence.assertion_result?.negative_evidence_scan ?? evidence.negative_evidence_scan;
|
|
193
200
|
if (!scan) {
|
|
194
|
-
return [];
|
|
201
|
+
return [`proof layer ${layerId} evidence ${evidence.evidence_id} missing negative_evidence_scan`];
|
|
195
202
|
}
|
|
196
203
|
const acId = proofLayerAcId(layerId);
|
|
197
204
|
const label = `proof layer ${layerId} evidence ${evidence.evidence_id}`;
|
|
@@ -205,6 +212,14 @@ export function evaluateNegativeEvidence(evidence, layerId) {
|
|
|
205
212
|
if (!scan.target_ac_ids.includes(acId)) {
|
|
206
213
|
findings.push(`${label} negative evidence scan target ACs ${scan.target_ac_ids.join(", ") || "(none)"} do not include ${acId}`);
|
|
207
214
|
}
|
|
215
|
+
const targetLayers = (scan.target_proof_layers ?? []).map(normalizeProofLayerId);
|
|
216
|
+
const normalizedLayerId = normalizeProofLayerId(layerId);
|
|
217
|
+
if (targetLayers.length === 0) {
|
|
218
|
+
findings.push(`${label} negative evidence scan target proof layers are missing; expected ${normalizedLayerId}`);
|
|
219
|
+
}
|
|
220
|
+
else if (!targetLayers.includes(normalizedLayerId) && !targetLayers.includes(proofLayerName(layerId))) {
|
|
221
|
+
findings.push(`${label} negative evidence scan target proof layers ${targetLayers.join(", ") || "(none)"} do not include ${normalizedLayerId}`);
|
|
222
|
+
}
|
|
208
223
|
for (const finding of scan.forbidden_findings ?? []) {
|
|
209
224
|
if (finding.status === "found") {
|
|
210
225
|
findings.push(`${label} negative evidence found forbidden text ${finding.forbidden_text ?? finding.id}: ${finding.actual ?? ""}`.trim());
|
|
@@ -226,7 +241,7 @@ function invalidEvidenceReason(evidence, layerName) {
|
|
|
226
241
|
if (genericShortcut) {
|
|
227
242
|
return `matches invalid completion evidence ${genericShortcut}`;
|
|
228
243
|
}
|
|
229
|
-
if ((layerName === "
|
|
244
|
+
if ((layerName === "worker_runtime" || layerName === "integration" || layerName === "ui_browser") && /\b(unit|mock|viewmodel)\b/i.test(evidence.type)) {
|
|
230
245
|
return "unit, mock or viewmodel evidence is auxiliary only for runtime, worker, integration and UI layers";
|
|
231
246
|
}
|
|
232
247
|
if (layerName === "ui_browser" && /\b(api|schema)\b/i.test(evidence.type)) {
|
|
@@ -235,7 +250,7 @@ function invalidEvidenceReason(evidence, layerName) {
|
|
|
235
250
|
if (layerName === "ui_browser" && /^screenshot$/i.test(evidence.type)) {
|
|
236
251
|
return "screenshot-only evidence cannot satisfy UI Path AC";
|
|
237
252
|
}
|
|
238
|
-
if ((layerName === "
|
|
253
|
+
if ((layerName === "worker_runtime" || layerName === "data_artifact" || layerName === "security_redaction") && /\b(ui|browser|playwright|screenshot)\b/i.test(evidence.type)) {
|
|
239
254
|
return "UI-only evidence cannot satisfy runtime, data or security proof layers";
|
|
240
255
|
}
|
|
241
256
|
return undefined;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ExecutionAttempt, SuperpowersAttemptMode, SuperpowersTaskState } from "./superpowers-task-state-schema.js";
|
|
2
|
+
export declare function startSuperpowersAttempt(workdir: string, state: SuperpowersTaskState, mode?: SuperpowersAttemptMode): Promise<ExecutionAttempt>;
|
|
3
|
+
export declare function startAndSaveSuperpowersAttempt(workdir: string, mode?: SuperpowersAttemptMode): Promise<ExecutionAttempt>;
|
|
4
|
+
export declare function computeSourceBundleHash(state: SuperpowersTaskState, specsHash?: string): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { appendSuperpowersEvent } from "./superpowers-task-events.js";
|
|
4
|
+
import { requiredCommandSpecsHash } from "./superpowers-task-command-specs.js";
|
|
5
|
+
import { loadSuperpowersState, saveSuperpowersState, sha256, stableJson } from "./superpowers-task-state.js";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export async function startSuperpowersAttempt(workdir, state, mode = "product_task") {
|
|
8
|
+
const now = new Date().toISOString();
|
|
9
|
+
const specsHash = requiredCommandSpecsHash(state.required_command_specs ?? []);
|
|
10
|
+
const sourceBundleHash = computeSourceBundleHash(state, specsHash);
|
|
11
|
+
const git = await readGitIdentity(workdir);
|
|
12
|
+
const worktreeFingerprint = sha256(stableJson(git));
|
|
13
|
+
const attemptOrdinal = (state.attempts ?? []).length + 1;
|
|
14
|
+
const attempt = {
|
|
15
|
+
task_attempt_id: `ATT-${compactDate(now)}-${attemptOrdinal}-${sourceBundleHash.slice(0, 8)}`,
|
|
16
|
+
source_bundle_hash: sourceBundleHash,
|
|
17
|
+
product_source_hash: state.sources.product_architecture_source?.sha256 ?? "",
|
|
18
|
+
technical_plan_hash: state.sources.technical_realization_plan?.sha256 ?? "",
|
|
19
|
+
acceptance_checklist_hash: state.sources.acceptance_checklist?.sha256 ?? "",
|
|
20
|
+
git_head: git.git_head,
|
|
21
|
+
git_status_short: git.git_status_short,
|
|
22
|
+
tracked_diff_hash: git.tracked_diff_hash,
|
|
23
|
+
relevant_untracked_hash: git.relevant_untracked_hash,
|
|
24
|
+
untracked_relevant_hash: git.relevant_untracked_hash,
|
|
25
|
+
worktree_fingerprint: worktreeFingerprint,
|
|
26
|
+
started_at: now,
|
|
27
|
+
ended_at: null,
|
|
28
|
+
finalized_at: null,
|
|
29
|
+
required_command_specs_hash: specsHash,
|
|
30
|
+
mode,
|
|
31
|
+
changed_files: git.changed_files
|
|
32
|
+
};
|
|
33
|
+
state.attempts = [...(state.attempts ?? []).filter((item) => item.task_attempt_id !== attempt.task_attempt_id), attempt];
|
|
34
|
+
state.current_attempt_id = attempt.task_attempt_id;
|
|
35
|
+
return attempt;
|
|
36
|
+
}
|
|
37
|
+
export async function startAndSaveSuperpowersAttempt(workdir, mode = "product_task") {
|
|
38
|
+
const state = await loadSuperpowersState(workdir);
|
|
39
|
+
const attempt = await startSuperpowersAttempt(workdir, state, mode);
|
|
40
|
+
await saveSuperpowersState(workdir, state);
|
|
41
|
+
await appendSuperpowersEvent(workdir, "attempt_started", { task_attempt_id: attempt.task_attempt_id });
|
|
42
|
+
return attempt;
|
|
43
|
+
}
|
|
44
|
+
export function computeSourceBundleHash(state, specsHash = requiredCommandSpecsHash(state.required_command_specs ?? [])) {
|
|
45
|
+
return sha256(stableJson({
|
|
46
|
+
product_source_hash: state.sources.product_architecture_source?.sha256 ?? "",
|
|
47
|
+
technical_plan_hash: state.sources.technical_realization_plan?.sha256 ?? "",
|
|
48
|
+
acceptance_checklist_hash: state.sources.acceptance_checklist?.sha256 ?? "",
|
|
49
|
+
required_command_specs_hash: specsHash
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
async function readGitIdentity(workdir) {
|
|
53
|
+
const head = await git(workdir, ["rev-parse", "HEAD"]);
|
|
54
|
+
if (!head.ok) {
|
|
55
|
+
return {
|
|
56
|
+
git_head: "no-git",
|
|
57
|
+
git_status_short: "no-git",
|
|
58
|
+
tracked_diff_hash: sha256("no-git-tracked-diff"),
|
|
59
|
+
relevant_untracked_hash: "ignored:no-git",
|
|
60
|
+
changed_files: []
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const status = await git(workdir, ["status", "--short"]);
|
|
64
|
+
const diff = await git(workdir, ["diff", "--binary"]);
|
|
65
|
+
const stagedDiff = await git(workdir, ["diff", "--cached", "--binary"]);
|
|
66
|
+
const untracked = await git(workdir, ["ls-files", "--others", "--exclude-standard"]);
|
|
67
|
+
const trackedNames = await git(workdir, ["diff", "--name-only"]);
|
|
68
|
+
const stagedNames = await git(workdir, ["diff", "--cached", "--name-only"]);
|
|
69
|
+
const changedFiles = unique([
|
|
70
|
+
...lines(trackedNames.ok ? trackedNames.stdout : ""),
|
|
71
|
+
...lines(stagedNames.ok ? stagedNames.stdout : ""),
|
|
72
|
+
...lines(untracked.ok ? untracked.stdout : "")
|
|
73
|
+
]);
|
|
74
|
+
return {
|
|
75
|
+
git_head: head.stdout.trim(),
|
|
76
|
+
git_status_short: status.ok ? status.stdout.trim() : "git-status-unavailable",
|
|
77
|
+
tracked_diff_hash: sha256(`${diff.ok ? diff.stdout : "git-diff-unavailable"}\n${stagedDiff.ok ? stagedDiff.stdout : ""}`),
|
|
78
|
+
relevant_untracked_hash: untracked.ok && untracked.stdout.trim() ? sha256(untracked.stdout.trim()) : "none",
|
|
79
|
+
changed_files: changedFiles
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function git(workdir, args) {
|
|
83
|
+
try {
|
|
84
|
+
const result = await execFileAsync("git", args, { cwd: workdir, windowsHide: true, timeout: 10000 });
|
|
85
|
+
return { ok: true, stdout: result.stdout };
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return { ok: false, stdout: "" };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function compactDate(value) {
|
|
92
|
+
return value.replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
93
|
+
}
|
|
94
|
+
function lines(value) {
|
|
95
|
+
return value
|
|
96
|
+
.split(/\r?\n/)
|
|
97
|
+
.map((item) => item.trim().replace(/\\/g, "/"))
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
}
|
|
100
|
+
function unique(values) {
|
|
101
|
+
return [...new Set(values.filter(Boolean))];
|
|
102
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { RequiredCommandSpec, SuperpowersTaskState } from "./superpowers-task-state-schema.js";
|
|
2
|
+
export declare function deriveRequiredCommandSpecs(state: SuperpowersTaskState): RequiredCommandSpec[];
|
|
3
|
+
export declare function requiredCommandSpecsHash(specs: RequiredCommandSpec[]): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { stableJson } from "./superpowers-task-state.js";
|
|
3
|
+
export function deriveRequiredCommandSpecs(state) {
|
|
4
|
+
return Object.entries(state.graph.acceptance_criteria).flatMap(([acId, ac]) => {
|
|
5
|
+
const proofLayers = [...new Set((ac.required_proof_layers ?? []).filter(Boolean))];
|
|
6
|
+
if (proofLayers.length === 0) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const spec = {
|
|
10
|
+
ac_id: acId,
|
|
11
|
+
proof_layers: proofLayers,
|
|
12
|
+
command: ac.assertion_command ?? "",
|
|
13
|
+
assertion_artifacts: ac.assertion_artifacts ?? [],
|
|
14
|
+
required_test_ids: ac.required_test_ids ?? [],
|
|
15
|
+
machine_blocking: ac.machine_blocking !== false,
|
|
16
|
+
assertion_result_required: ac.assertion_result_required !== false,
|
|
17
|
+
positive_assertions: ac.positive_assertions ?? [],
|
|
18
|
+
negative_assertions: ac.negative_assertions ?? [],
|
|
19
|
+
invalid_completion_signals: ac.invalid_completion_signals ?? [],
|
|
20
|
+
final_evidence_expected: ac.final_evidence_expected ?? []
|
|
21
|
+
};
|
|
22
|
+
return [{ command_spec_id: commandSpecId(spec), ...spec }];
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function requiredCommandSpecsHash(specs) {
|
|
26
|
+
return sha256(stableJson(specs.map((spec) => ({
|
|
27
|
+
...spec,
|
|
28
|
+
proof_layers: [...(spec.proof_layers ?? [])].sort(),
|
|
29
|
+
assertion_artifacts: [...(spec.assertion_artifacts ?? [])].sort(),
|
|
30
|
+
required_test_ids: [...(spec.required_test_ids ?? [])].sort(),
|
|
31
|
+
positive_assertions: [...(spec.positive_assertions ?? [])].sort(),
|
|
32
|
+
negative_assertions: [...(spec.negative_assertions ?? [])].sort(),
|
|
33
|
+
invalid_completion_signals: [...(spec.invalid_completion_signals ?? [])].sort(),
|
|
34
|
+
final_evidence_expected: [...(spec.final_evidence_expected ?? [])].sort()
|
|
35
|
+
}))));
|
|
36
|
+
}
|
|
37
|
+
function commandSpecId(spec) {
|
|
38
|
+
return sha256([
|
|
39
|
+
spec.ac_id,
|
|
40
|
+
spec.proof_layers.join(","),
|
|
41
|
+
spec.command,
|
|
42
|
+
spec.assertion_artifacts.join(","),
|
|
43
|
+
spec.required_test_ids.join(","),
|
|
44
|
+
spec.positive_assertions.join(","),
|
|
45
|
+
spec.negative_assertions.join(","),
|
|
46
|
+
spec.invalid_completion_signals.join(","),
|
|
47
|
+
spec.final_evidence_expected.join(",")
|
|
48
|
+
].join("\n"));
|
|
49
|
+
}
|
|
50
|
+
function sha256(value) {
|
|
51
|
+
return createHash("sha256").update(value).digest("hex");
|
|
52
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import { type SuperpowersTaskState } from "./superpowers-task-state-schema.js";
|
|
2
|
-
|
|
2
|
+
import type { SuperpowersAttemptMode } from "./superpowers-task-state-schema.js";
|
|
3
|
+
export declare function compileSuperpowersTask(workdir: string, options?: {
|
|
4
|
+
mode?: SuperpowersAttemptMode;
|
|
5
|
+
}): Promise<SuperpowersTaskState>;
|
|
3
6
|
export declare function computeScopeConflicts(state: SuperpowersTaskState): string[];
|
|
@@ -3,9 +3,11 @@ import { pathExists, readText } from "./fs.js";
|
|
|
3
3
|
import { appendSuperpowersEvent } from "./superpowers-task-events.js";
|
|
4
4
|
import { compileError, compileReportSuffix, throwCompileErrors } from "./superpowers-task-compile-diagnostics.js";
|
|
5
5
|
import { validateCompiledSources } from "./superpowers-task-compile-guards.js";
|
|
6
|
+
import { startSuperpowersAttempt } from "./superpowers-task-attempt.js";
|
|
7
|
+
import { deriveRequiredCommandSpecs } from "./superpowers-task-command-specs.js";
|
|
6
8
|
import { loadSuperpowersState, recomputeStatuses, saveSuperpowersState, refreshSourceHashes } from "./superpowers-task-state.js";
|
|
7
9
|
import { DEFAULT_LAYERS, parseAcceptanceCriteria, parsePlanItems, parseProductArchitectureScope } from "./superpowers-task-source-compile.js";
|
|
8
|
-
export async function compileSuperpowersTask(workdir) {
|
|
10
|
+
export async function compileSuperpowersTask(workdir, options = {}) {
|
|
9
11
|
const state = await loadSuperpowersState(workdir);
|
|
10
12
|
await refreshSourceHashes(workdir, state);
|
|
11
13
|
await assertRequiredSourcesExist(workdir, state);
|
|
@@ -40,9 +42,13 @@ export async function compileSuperpowersTask(workdir) {
|
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
state.graph.edges = Object.entries(planItems).flatMap(([planId, item]) => item.related_acs.map((acId) => ({ from: planId, to: acId, type: "supports" })));
|
|
45
|
+
state.required_command_specs = deriveRequiredCommandSpecs(state);
|
|
46
|
+
state.command_runs = [];
|
|
47
|
+
state.negative_evidence_records = [];
|
|
43
48
|
state.delivery.scope_conflicts = computeScopeConflicts(state);
|
|
44
49
|
state.progress = compileProgress(state);
|
|
45
50
|
recomputeStatuses(state);
|
|
51
|
+
await startSuperpowersAttempt(workdir, state, options.mode ?? "product_task");
|
|
46
52
|
await saveSuperpowersState(workdir, state);
|
|
47
53
|
await appendSuperpowersEvent(workdir, "graph_compiled", {
|
|
48
54
|
plan_items: Object.keys(planItems).length,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SuperpowersTaskState } from "./superpowers-task-state-schema.js";
|
|
2
|
+
export interface SuperpowersContradictionScan {
|
|
3
|
+
errors: string[];
|
|
4
|
+
historicalCompleteIgnored: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function scanSuperpowersContradictions(workdir: string, state: SuperpowersTaskState): Promise<SuperpowersContradictionScan>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { listFiles, pathExists, readText } from "./fs.js";
|
|
3
|
+
const OWNER_SURFACE_FORBIDDEN_STATES = ["尚未运行自测", "运行未记录", "未验证", "不可用", "暂不可用", "页面无明显变化"];
|
|
4
|
+
export async function scanSuperpowersContradictions(workdir, state) {
|
|
5
|
+
const files = await listFiles(workdir);
|
|
6
|
+
const errors = [
|
|
7
|
+
...scanFinalStateContradictions(state),
|
|
8
|
+
...(await scanCurrentFailureArtifacts(workdir, files))
|
|
9
|
+
];
|
|
10
|
+
const historicalCompleteIgnored = errors.length > 0 && (await hasHistoricalCompleteEvent(workdir));
|
|
11
|
+
if (historicalCompleteIgnored) {
|
|
12
|
+
errors.push("historical_complete_ignored: Historical stale completion event detected and ignored. Current recomputed product_goal_complete=false.");
|
|
13
|
+
}
|
|
14
|
+
if (errors.length > 0) {
|
|
15
|
+
errors.push("workflow_gate_bug_prevented: current contradiction scan prevents product_goal_complete=true.");
|
|
16
|
+
}
|
|
17
|
+
return { errors: unique(errors), historicalCompleteIgnored };
|
|
18
|
+
}
|
|
19
|
+
function scanFinalStateContradictions(state) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
if (state.meta?.product_goal_complete === false && state.final?.product_goal_complete === true) {
|
|
22
|
+
errors.push("current contradiction task_state_product_goal_mismatch: meta product_goal_complete=false but final product_goal_complete=true.");
|
|
23
|
+
}
|
|
24
|
+
if (state.meta?.acceptance_target_status && state.final?.acceptance_target_status && state.meta.acceptance_target_status !== state.final.acceptance_target_status) {
|
|
25
|
+
errors.push(`current contradiction task_state_acceptance_status_mismatch: meta=${state.meta.acceptance_target_status} final=${state.final.acceptance_target_status}.`);
|
|
26
|
+
}
|
|
27
|
+
for (const commandRun of state.command_runs ?? []) {
|
|
28
|
+
if (Number(commandRun.exit_code) !== 0) {
|
|
29
|
+
errors.push(`current contradiction command_run_failed: ${commandRun.command_run_id} ac=${commandRun.ac_id} proof_layer=${commandRun.proof_layer} exit_code=${commandRun.exit_code}.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return errors;
|
|
33
|
+
}
|
|
34
|
+
async function scanCurrentFailureArtifacts(workdir, files) {
|
|
35
|
+
const errors = [];
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const relative = slash(path.relative(workdir, file));
|
|
38
|
+
if (relative.endsWith(".last-run.json")) {
|
|
39
|
+
errors.push(...(await scanPlaywrightLastRun(file, relative)));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (/test-results\/.*error-context\.md$/i.test(relative)) {
|
|
43
|
+
errors.push(...(await scanOwnerDomErrorContext(file, relative)));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (isFailureJsonCandidate(relative)) {
|
|
47
|
+
errors.push(...(await scanFailedJsonArtifact(file, relative)));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return errors;
|
|
51
|
+
}
|
|
52
|
+
async function scanPlaywrightLastRun(file, relative) {
|
|
53
|
+
const parsed = await readJsonRecord(file);
|
|
54
|
+
if (!parsed) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const status = String(parsed.status ?? parsed.outcome ?? "").toLowerCase();
|
|
58
|
+
const failedTests = Array.isArray(parsed.failedTests) ? parsed.failedTests : Array.isArray(parsed.failed_tests) ? parsed.failed_tests : [];
|
|
59
|
+
if (status !== "failed" && failedTests.length === 0) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return [
|
|
63
|
+
`current contradiction playwright_last_run_failed: ${relative} status=${status || "unknown"} failed_tests=${failedTests
|
|
64
|
+
.map(String)
|
|
65
|
+
.join(", ") || "(unspecified)"}.`
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
async function scanOwnerDomErrorContext(file, relative) {
|
|
69
|
+
const text = await readText(file);
|
|
70
|
+
const findings = OWNER_SURFACE_FORBIDDEN_STATES.filter((state) => text.includes(state));
|
|
71
|
+
if (findings.length === 0) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
return [`current contradiction owner_dom_forbidden_state: ${relative} contains ${findings.join(", ")}.`];
|
|
75
|
+
}
|
|
76
|
+
async function scanFailedJsonArtifact(file, relative) {
|
|
77
|
+
const parsed = await readJsonRecord(file);
|
|
78
|
+
if (!parsed) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const text = JSON.stringify(parsed);
|
|
82
|
+
const status = String(parsed.status ?? parsed.outcome ?? parsed.result ?? "").toLowerCase();
|
|
83
|
+
const failures = Number(parsed.failures ?? parsed.failed ?? parsed.failed_count ?? 0);
|
|
84
|
+
if (status !== "failed" && failures <= 0 && !/"failed"/i.test(text)) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return [`current contradiction failed_test_result_artifact: ${relative} reports failed status.`];
|
|
88
|
+
}
|
|
89
|
+
function isFailureJsonCandidate(relative) {
|
|
90
|
+
return (/\.json$/i.test(relative) &&
|
|
91
|
+
/(^|\/)(test-results|playwright-report|junit|command-runs|negative-evidence|negative_evidence)(\/|$)/i.test(relative));
|
|
92
|
+
}
|
|
93
|
+
async function hasHistoricalCompleteEvent(workdir) {
|
|
94
|
+
const eventsPath = path.join(workdir, "events.ndjson");
|
|
95
|
+
if (!(await pathExists(eventsPath))) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const content = await readText(eventsPath);
|
|
99
|
+
return content
|
|
100
|
+
.split(/\r?\n/)
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.some((line) => {
|
|
103
|
+
try {
|
|
104
|
+
const event = JSON.parse(line);
|
|
105
|
+
return String(event.event ?? event.event_type ?? event.type ?? "") === "final_gate" && event.product_goal_complete === true;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return /final_gate/.test(line) && /product_goal_complete["']?\s*:\s*true/.test(line);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async function readJsonRecord(file) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(await readText(file));
|
|
115
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function slash(value) {
|
|
122
|
+
return value.split(path.sep).join("/");
|
|
123
|
+
}
|
|
124
|
+
function unique(values) {
|
|
125
|
+
return [...new Set(values.filter(Boolean))];
|
|
126
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SuperpowersEvidenceRecord, SuperpowersTaskState } from "./superpowers-task-state-schema.js";
|
|
2
|
+
export declare function evaluateCurrentAttemptEvidence(state: SuperpowersTaskState, evidence: SuperpowersEvidenceRecord, layerId: string): string[];
|
|
3
|
+
export declare function evaluateCurrentAttemptArtifact(workdir: string, evidence: SuperpowersEvidenceRecord, layerId: string): Promise<string[]>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathExists, readText } from "./fs.js";
|
|
4
|
+
import { normalizeProofLayerName } from "./superpowers-task-fields.js";
|
|
5
|
+
export function evaluateCurrentAttemptEvidence(state, evidence, layerId) {
|
|
6
|
+
if (!state.current_attempt_id) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const failures = [];
|
|
10
|
+
const label = `proof layer ${layerId} evidence ${evidence.evidence_id}`;
|
|
11
|
+
if (evidence.schema_version !== "evidence-record-v2") {
|
|
12
|
+
failures.push(`${label} must be EvidenceRecordV2 for current-attempt machine completion`);
|
|
13
|
+
}
|
|
14
|
+
if (evidence.task_attempt_id !== state.current_attempt_id) {
|
|
15
|
+
failures.push(`${label} stale evidence from old attempt ${evidence.task_attempt_id || "(missing)"}; expected current attempt ${state.current_attempt_id}`);
|
|
16
|
+
}
|
|
17
|
+
for (const field of [
|
|
18
|
+
"source_bundle_hash",
|
|
19
|
+
"product_source_hash",
|
|
20
|
+
"technical_plan_hash",
|
|
21
|
+
"acceptance_checklist_hash",
|
|
22
|
+
"git_head",
|
|
23
|
+
"worktree_fingerprint",
|
|
24
|
+
"command_spec_id",
|
|
25
|
+
"command_run_id",
|
|
26
|
+
"command_line",
|
|
27
|
+
"artifact_path",
|
|
28
|
+
"artifact_sha256",
|
|
29
|
+
"artifact_mtime"
|
|
30
|
+
]) {
|
|
31
|
+
if (!evidence[field]) {
|
|
32
|
+
failures.push(`${label} EvidenceRecordV2 missing ${field}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const attempt = state.attempts?.find((item) => item.task_attempt_id === state.current_attempt_id);
|
|
36
|
+
if (attempt && evidence.source_bundle_hash && evidence.source_bundle_hash !== attempt.source_bundle_hash) {
|
|
37
|
+
failures.push(`${label} stale evidence source_bundle_hash mismatch for current attempt`);
|
|
38
|
+
}
|
|
39
|
+
if (attempt && evidence.product_source_hash && evidence.product_source_hash !== attempt.product_source_hash) {
|
|
40
|
+
failures.push(`${label} stale evidence product_source_hash mismatch for current attempt`);
|
|
41
|
+
}
|
|
42
|
+
if (attempt && evidence.technical_plan_hash && evidence.technical_plan_hash !== attempt.technical_plan_hash) {
|
|
43
|
+
failures.push(`${label} stale evidence technical_plan_hash mismatch for current attempt`);
|
|
44
|
+
}
|
|
45
|
+
if (attempt && evidence.acceptance_checklist_hash && evidence.acceptance_checklist_hash !== attempt.acceptance_checklist_hash) {
|
|
46
|
+
failures.push(`${label} stale evidence acceptance_checklist_hash mismatch for current attempt`);
|
|
47
|
+
}
|
|
48
|
+
if (attempt && evidence.git_head && evidence.git_head !== attempt.git_head) {
|
|
49
|
+
failures.push(`${label} stale evidence git_head mismatch for current attempt`);
|
|
50
|
+
}
|
|
51
|
+
if (attempt && evidence.worktree_fingerprint && evidence.worktree_fingerprint !== attempt.worktree_fingerprint) {
|
|
52
|
+
failures.push(`${label} stale evidence worktree_fingerprint mismatch for current attempt`);
|
|
53
|
+
}
|
|
54
|
+
if (attempt && evidence.artifact_mtime && Date.parse(evidence.artifact_mtime) < Date.parse(attempt.started_at)) {
|
|
55
|
+
failures.push(`${label} stale evidence artifact_mtime predates current attempt`);
|
|
56
|
+
}
|
|
57
|
+
const commandRun = state.command_runs?.find((item) => item.command_run_id === evidence.command_run_id);
|
|
58
|
+
if (!commandRun) {
|
|
59
|
+
failures.push(`${label} missing command run ${evidence.command_run_id || "(missing)"}`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
if (commandRun.task_attempt_id !== state.current_attempt_id) {
|
|
63
|
+
failures.push(`${label} command run ${commandRun.command_run_id} is from old attempt ${commandRun.task_attempt_id}`);
|
|
64
|
+
}
|
|
65
|
+
if (commandRun.exit_code !== 0) {
|
|
66
|
+
failures.push(`${label} command run ${commandRun.command_run_id} exit_code=${commandRun.exit_code}; expected 0`);
|
|
67
|
+
}
|
|
68
|
+
if (commandRun.command_spec_id !== evidence.command_spec_id) {
|
|
69
|
+
failures.push(`${label} command_spec_id mismatch between evidence and command run`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const commandSpec = state.required_command_specs?.find((item) => item.command_spec_id === evidence.command_spec_id);
|
|
73
|
+
if (!commandSpec) {
|
|
74
|
+
failures.push(`${label} missing required command spec ${evidence.command_spec_id || "(missing)"}`);
|
|
75
|
+
}
|
|
76
|
+
else if (commandSpec.ac_id !== proofLayerAcId(layerId) || !commandSpec.proof_layers.map(normalizeProofLayerName).includes(proofLayerName(layerId))) {
|
|
77
|
+
failures.push(`${label} command spec does not cover ${layerId}`);
|
|
78
|
+
}
|
|
79
|
+
const ac = state.graph.acceptance_criteria?.[proofLayerAcId(layerId)];
|
|
80
|
+
const targetAcIds = evidence.target_ac_ids ?? evidence.assertion_result?.target_ac_ids ?? [];
|
|
81
|
+
if (!targetAcIds.includes(proofLayerAcId(layerId))) {
|
|
82
|
+
failures.push(`${label} target_ac_ids ${targetAcIds.join(", ") || "(none)"} do not include ${proofLayerAcId(layerId)}`);
|
|
83
|
+
}
|
|
84
|
+
const expectedPiIds = ac?.related_plan_items ?? [];
|
|
85
|
+
const targetPiIds = evidence.target_pi_ids ?? evidence.assertion_result?.target_pi_ids ?? [];
|
|
86
|
+
for (const piId of expectedPiIds) {
|
|
87
|
+
if (!targetPiIds.includes(piId)) {
|
|
88
|
+
failures.push(`${label} target_pi_ids ${targetPiIds.join(", ") || "(none)"} do not include related plan item ${piId}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const targetLayers = (evidence.target_proof_layers ?? evidence.assertion_result?.target_proof_layers ?? []).map(normalizeLayerId);
|
|
92
|
+
if (!targetLayers.includes(normalizeLayerId(layerId)) && !targetLayers.includes(proofLayerName(layerId))) {
|
|
93
|
+
failures.push(`${label} target_proof_layers ${targetLayers.join(", ") || "(none)"} do not include ${layerId}`);
|
|
94
|
+
}
|
|
95
|
+
if (evidence.assertion_result?.schema_version !== "assertion-result-v2") {
|
|
96
|
+
failures.push(`${label} assertion_result.schema_version must be assertion-result-v2 for current-attempt machine completion`);
|
|
97
|
+
}
|
|
98
|
+
return failures;
|
|
99
|
+
}
|
|
100
|
+
export async function evaluateCurrentAttemptArtifact(workdir, evidence, layerId) {
|
|
101
|
+
const label = `proof layer ${layerId} evidence ${evidence.evidence_id}`;
|
|
102
|
+
return validateArtifact(workdir, evidence, label);
|
|
103
|
+
}
|
|
104
|
+
async function validateArtifact(workdir, evidence, label) {
|
|
105
|
+
const failures = [];
|
|
106
|
+
if (!evidence.artifact_path) {
|
|
107
|
+
failures.push(`${label} EvidenceRecordV2 missing artifact_path`);
|
|
108
|
+
return failures;
|
|
109
|
+
}
|
|
110
|
+
const artifact = await resolveArtifactPath(workdir, evidence.artifact_path);
|
|
111
|
+
if (!artifact) {
|
|
112
|
+
failures.push(`${label} artifact_path does not exist: ${evidence.artifact_path}`);
|
|
113
|
+
return failures;
|
|
114
|
+
}
|
|
115
|
+
if (evidence.artifact_sha256) {
|
|
116
|
+
const actual = createHash("sha256").update(await readText(artifact)).digest("hex");
|
|
117
|
+
if (actual !== evidence.artifact_sha256) {
|
|
118
|
+
failures.push(`${label} stale evidence artifact_sha256 mismatch for ${evidence.artifact_path}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return failures;
|
|
122
|
+
}
|
|
123
|
+
async function resolveArtifactPath(workdir, artifactPath) {
|
|
124
|
+
const candidates = path.isAbsolute(artifactPath) ? [artifactPath] : [path.join(workdir, artifactPath), path.join(projectRootFromWorkdir(workdir), artifactPath)];
|
|
125
|
+
for (const candidate of candidates) {
|
|
126
|
+
if (await pathExists(candidate)) {
|
|
127
|
+
return candidate;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
function proofLayerName(layerId) {
|
|
133
|
+
const raw = layerId.includes(".") ? layerId.slice(layerId.lastIndexOf(".") + 1) : layerId;
|
|
134
|
+
return normalizeProofLayerName(raw);
|
|
135
|
+
}
|
|
136
|
+
function proofLayerAcId(layerId) {
|
|
137
|
+
return layerId.includes(".") ? layerId.slice(0, layerId.lastIndexOf(".")) : "";
|
|
138
|
+
}
|
|
139
|
+
function normalizeLayerId(layerId) {
|
|
140
|
+
if (!layerId.includes(".")) {
|
|
141
|
+
return normalizeProofLayerName(layerId);
|
|
142
|
+
}
|
|
143
|
+
const acId = proofLayerAcId(layerId);
|
|
144
|
+
return `${acId}.${proofLayerName(layerId)}`;
|
|
145
|
+
}
|
|
146
|
+
function projectRootFromWorkdir(workdir) {
|
|
147
|
+
const normalized = workdir.replace(/\\/g, "/");
|
|
148
|
+
const marker = "/tmp/ty-context/plan-acceptance/";
|
|
149
|
+
const index = normalized.lastIndexOf(marker);
|
|
150
|
+
if (index < 0) {
|
|
151
|
+
return workdir;
|
|
152
|
+
}
|
|
153
|
+
return normalized.slice(0, index);
|
|
154
|
+
}
|