@workit-dev/core 0.0.1 → 1.0.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/LICENSE +21 -0
- package/README.md +22 -43
- package/package.json +56 -6
- package/schemas/config.json +74 -0
- package/schemas/vcs.json +79 -0
- package/schemas/workspaces.json +1029 -0
- package/schemas/youtrack.json +84 -0
- package/scripts/analyze-release-scope.ts +215 -0
- package/scripts/bundled-deps.json +131 -0
- package/scripts/fixtures/sample-plan.md +15 -0
- package/scripts/fixtures/sample-sdd/progress.md +2 -0
- package/scripts/install-codex-plugin.sh +28 -0
- package/scripts/install-cursor-plugin.sh +146 -0
- package/scripts/install-opencode-plugin.sh +79 -0
- package/scripts/install-pi-package.sh +56 -0
- package/scripts/lib/config-dir.sh +28 -0
- package/scripts/publish-changed-packages.ts +395 -0
- package/scripts/rewrite-workspace-deps.ts +53 -0
- package/scripts/run-cursor-mcp.sh +6 -0
- package/scripts/sync-release-manifests.ts +82 -0
- package/scripts/sync-runtime.sh +142 -0
- package/scripts/validate-cursor-marketplace.ts +171 -0
- package/scripts/verify-bundle-sources.ts +134 -0
- package/scripts/write-schemas.ts +16 -0
- package/skills/workit-architecture/SKILL.md +72 -0
- package/skills/workit-architecture/agents/openai.yaml +3 -0
- package/skills/workit-architecture/references/instruction-files.md +52 -0
- package/skills/workit-architecture/references/vocabulary.md +53 -0
- package/skills/workit-bdd/SKILL.md +53 -0
- package/skills/workit-continue/SKILL.md +54 -0
- package/skills/workit-debug/SKILL.md +45 -0
- package/skills/workit-deslop/SKILL.md +58 -0
- package/skills/workit-fanout/SKILL.md +71 -0
- package/skills/workit-fanout/references/brief.md +141 -0
- package/skills/workit-fanout/references/orchestration.md +169 -0
- package/skills/workit-implement/SKILL.md +71 -0
- package/skills/workit-retro/SKILL.md +72 -0
- package/skills/workit-retro/agents/openai.yaml +3 -0
- package/skills/workit-retro/references/sources.md +35 -0
- package/skills/workit-retro/references/steering.md +39 -0
- package/skills/workit-review/SKILL.md +66 -0
- package/skills/workit-review/references/impact.md +24 -0
- package/skills/workit-shape/SKILL.md +71 -0
- package/skills/workit-shape/references/diagrams.md +17 -0
- package/skills/workit-shape/references/knowledge.md +61 -0
- package/skills/workit-shape/references/mockups.md +15 -0
- package/skills/workit-shape/references/slicing.md +44 -0
- package/skills/workit-ship/SKILL.md +71 -0
- package/skills/workit-ship/references/changelog.md +44 -0
- package/skills/workit-ship/references/pr-body.md +32 -0
- package/skills/workit-ship/references/release.md +39 -0
- package/skills/workit-test-audit/SKILL.md +54 -0
- package/skills/workit-verify-app/SKILL.md +68 -0
- package/skills/workit-verify-app/references/template.md +51 -0
- package/src/autonomy.ts +910 -0
- package/src/check-config.ts +613 -0
- package/src/checks.ts +534 -0
- package/src/ci-waiter.ts +159 -0
- package/src/core/boundary.ts +55 -0
- package/src/core/branch-policy.ts +112 -0
- package/src/core/branch.ts +1192 -0
- package/src/core/changelog-gate.ts +349 -0
- package/src/core/changelog-merge.ts +214 -0
- package/src/core/changelog.ts +392 -0
- package/src/core/cleanup.ts +638 -0
- package/src/core/cli-locate.ts +173 -0
- package/src/core/commit-flavors.ts +65 -0
- package/src/core/config-schemas.ts +653 -0
- package/src/core/config.ts +351 -0
- package/src/core/context-read.ts +144 -0
- package/src/core/docs-gate.ts +69 -0
- package/src/core/finding-evidence.ts +89 -0
- package/src/core/finding-queue.ts +36 -0
- package/src/core/git.ts +73 -0
- package/src/core/gitignore.ts +52 -0
- package/src/core/heavy-eta.ts +170 -0
- package/src/core/heavy-lease.ts +290 -0
- package/src/core/heavy-rules.ts +373 -0
- package/src/core/heavy-scope.ts +131 -0
- package/src/core/heavy-words.ts +83 -0
- package/src/core/heavy.ts +535 -0
- package/src/core/hygiene.ts +149 -0
- package/src/core/init.ts +218 -0
- package/src/core/live-session.ts +333 -0
- package/src/core/logger.ts +310 -0
- package/src/core/methods.ts +185 -0
- package/src/core/model-fallback.ts +208 -0
- package/src/core/node-resolve.mjs +249 -0
- package/src/core/operation-input.ts +632 -0
- package/src/core/package-root.ts +28 -0
- package/src/core/policy/attributes.ts +97 -0
- package/src/core/policy/bookkeeping.ts +119 -0
- package/src/core/policy/carry.ts +122 -0
- package/src/core/policy/check-evidence.ts +115 -0
- package/src/core/policy/current-judgment.ts +20 -0
- package/src/core/policy/derive.ts +387 -0
- package/src/core/policy/judgment.ts +246 -0
- package/src/core/policy/lane.ts +562 -0
- package/src/core/policy/pr-lane.ts +32 -0
- package/src/core/policy/substance.ts +365 -0
- package/src/core/policy-resolver.ts +94 -0
- package/src/core/pr-create.ts +448 -0
- package/src/core/pr-template.ts +162 -0
- package/src/core/release-notes.ts +197 -0
- package/src/core/release-tracks.ts +779 -0
- package/src/core/remotes.ts +436 -0
- package/src/core/repo-context.ts +542 -0
- package/src/core/repo-paths.ts +29 -0
- package/src/core/resources.ts +1299 -0
- package/src/core/role-models.ts +603 -0
- package/src/core/route-intent.ts +305 -0
- package/src/core/rules.ts +65 -0
- package/src/core/runtime-identity.ts +63 -0
- package/src/core/safe-write.ts +87 -0
- package/src/core/scope-glob.ts +56 -0
- package/src/core/scripts.ts +6 -0
- package/src/core/skill-manifests.ts +173 -0
- package/src/core/store-lock.ts +374 -0
- package/src/core/support-matrix.ts +24 -0
- package/src/core/task-context.ts +247 -0
- package/src/core/task-contract.ts +1279 -0
- package/src/core/task-delivery.ts +109 -0
- package/src/core/task-engine.ts +2248 -0
- package/src/core/task-evaluation.ts +1147 -0
- package/src/core/task-store.ts +2649 -0
- package/src/core/task-view.ts +180 -0
- package/src/core/templates.ts +22 -0
- package/src/core/tracker-issues.ts +174 -0
- package/src/core/vcs-config.ts +540 -0
- package/src/core/workers.ts +62 -0
- package/src/core/workit-paths.mjs +242 -0
- package/src/core/workspace-branch-policy.ts +123 -0
- package/src/core/workspaces.ts +1745 -0
- package/src/core/youtrack-backend.ts +288 -0
- package/src/core/youtrack-tools.ts +260 -0
- package/src/core/youtrack-writes.ts +73 -0
- package/src/core/youtrack-ytrack.ts +217 -0
- package/src/core/youtrack.ts +454 -0
- package/src/core/ytrack-skill.ts +297 -0
- package/src/core.ts +171 -0
- package/src/deslop/lex.ts +271 -0
- package/src/deslop/scan.ts +232 -0
- package/src/duration.ts +27 -0
- package/src/fanout-brief.ts +486 -0
- package/src/fanout-check.ts +455 -0
- package/src/fanout-landed.ts +370 -0
- package/src/fanout-stack.ts +108 -0
- package/src/fanout-status.ts +362 -0
- package/src/fanout-worktree.ts +893 -0
- package/src/fanout.ts +1473 -0
- package/src/forge/exec.ts +385 -0
- package/src/forge/github.ts +831 -0
- package/src/forge/gitlab.ts +906 -0
- package/src/forge/issue-forge.ts +209 -0
- package/src/forge/issue-link.ts +468 -0
- package/src/forge/issue-ops.ts +314 -0
- package/src/forge/merge-gate-base-red.ts +72 -0
- package/src/forge/pr-body.ts +100 -0
- package/src/forge/pr-edit.ts +292 -0
- package/src/forge/pr-ops.ts +1109 -0
- package/src/forge/pr-status.ts +10 -0
- package/src/forge/redact.ts +195 -0
- package/src/forge/report.ts +994 -0
- package/src/forge/reruns.ts +132 -0
- package/src/forge/resolve.ts +509 -0
- package/src/forge/types.ts +242 -0
- package/src/forge/verify.ts +398 -0
- package/src/git/default-base.ts +130 -0
- package/src/git/env-redirect.ts +28 -0
- package/src/git/facts.ts +122 -0
- package/src/git/ops.ts +1823 -0
- package/src/git/refs-read.ts +332 -0
- package/src/git/release.ts +1020 -0
- package/src/git/rev.ts +1175 -0
- package/src/git/session-hook.ts +597 -0
- package/src/git/stage.ts +63 -0
- package/src/hooks/agent-model.ts +149 -0
- package/src/hooks/budget-signal.ts +284 -0
- package/src/hooks/context.ts +461 -0
- package/src/hooks/cwd-normalize.ts +79 -0
- package/src/hooks/deadline.ts +228 -0
- package/src/hooks/delivery-verbs.ts +182 -0
- package/src/hooks/descriptor.ts +124 -0
- package/src/hooks/handle.ts +433 -0
- package/src/hooks/heavy-gate.ts +99 -0
- package/src/hooks/hook-runs.ts +130 -0
- package/src/hooks/hosts/claude-code.ts +380 -0
- package/src/hooks/hosts/codex.ts +372 -0
- package/src/hooks/hosts/cursor.ts +419 -0
- package/src/hooks/hosts/fields.ts +98 -0
- package/src/hooks/hosts/opencode.ts +79 -0
- package/src/hooks/hosts/pi.ts +78 -0
- package/src/hooks/index.ts +60 -0
- package/src/hooks/model-limit.ts +378 -0
- package/src/hooks/model-switch.ts +35 -0
- package/src/hooks/policy.ts +29 -0
- package/src/hooks/process.ts +48 -0
- package/src/hooks/protocol.ts +135 -0
- package/src/hooks/raw-git.ts +2289 -0
- package/src/hooks/repeat-failure.ts +115 -0
- package/src/hooks/run.ts +54 -0
- package/src/hooks/session-marker.ts +151 -0
- package/src/hooks/shell-words.ts +532 -0
- package/src/hooks/skill-nudge.ts +295 -0
- package/src/hooks/spawn-gate.ts +72 -0
- package/src/hooks/standing-signal.ts +99 -0
- package/src/hooks/standing.ts +225 -0
- package/src/hooks/steering.ts +74 -0
- package/src/hooks/stop.ts +408 -0
- package/src/hooks/worktree-confine.ts +415 -0
- package/src/hooks/write-gate.ts +1174 -0
- package/src/host-session.ts +459 -0
- package/src/knowledge.ts +368 -0
- package/src/ledger.ts +2913 -0
- package/src/stack.ts +2570 -0
- package/src/store/event-log.ts +217 -0
- package/src/store/notes.ts +29 -0
- package/src/store/patch.ts +135 -0
- package/src/store/paths.ts +368 -0
- package/src/store/reduce.ts +203 -0
- package/src/test-audit-mocks.ts +309 -0
- package/src/verdict-cover.ts +151 -0
- package/src/worktree-deps.ts +142 -0
- package/templates/headers.md +3 -0
- package/templates/hygiene/.editorconfig +8 -0
- package/templates/hygiene/.gitattributes +3 -0
- package/templates/hygiene/CHANGELOG.md +14 -0
- package/templates/hygiene/CONTRIBUTING.md +3 -0
- package/templates/hygiene/LICENSE +21 -0
- package/templates/hygiene/README.md +3 -0
- package/templates/issue-update.md +5 -0
- package/templates/plan-template.md +26 -0
- package/templates/spec-template.md +64 -0
- package/templates/workit-contract.md +13 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
// Is a changed file cosmetic (#333, review finding mvkcoxgb)? Each side of
|
|
2
|
+
// the file is reduced to its substance by a small lexer that knows the
|
|
3
|
+
// file's strings and comments: comments drop out, whitespace outside strings
|
|
4
|
+
// is normalized, strings stay verbatim. Equal substance on both sides is a
|
|
5
|
+
// cosmetic change. The lexer fails closed: a syntax it does not know keeps
|
|
6
|
+
// its comments, an indentation-sensitive or unknown type keeps all but
|
|
7
|
+
// trailing whitespace, and anything it cannot read (invalid UTF-8, a NUL
|
|
8
|
+
// byte, an unterminated string or comment, a regex across lines) is
|
|
9
|
+
// substantive. Languages it would read wrongly stay unlexed (C-family, Rust,
|
|
10
|
+
// SQL, shell, Ruby, YAML): only trailing whitespace is cosmetic there.
|
|
11
|
+
|
|
12
|
+
type Syntax = {
|
|
13
|
+
/** Line comment openers. */
|
|
14
|
+
line: readonly string[];
|
|
15
|
+
/** Block comment delimiters (not nested). */
|
|
16
|
+
block: readonly (readonly [string, string])[];
|
|
17
|
+
/** String quotes with backslash escapes. */
|
|
18
|
+
quotes: string;
|
|
19
|
+
/** String quotes without escapes (TOML literal strings). */
|
|
20
|
+
literal?: string;
|
|
21
|
+
/** `"""` and `'''` strings. */
|
|
22
|
+
triple?: boolean;
|
|
23
|
+
/** JavaScript template literals with `${}` and regex literals. */
|
|
24
|
+
js?: boolean;
|
|
25
|
+
/** Go raw strings in backticks. */
|
|
26
|
+
rawBacktick?: boolean;
|
|
27
|
+
/** Strings may span lines. */
|
|
28
|
+
multiline?: boolean;
|
|
29
|
+
/** Python: an f-string with an unbalanced `{` holds a nested string this cannot read. */
|
|
30
|
+
fstring?: boolean;
|
|
31
|
+
/** CSS: an unquoted `url(` body is no string or comment this can read. */
|
|
32
|
+
cssUrl?: boolean;
|
|
33
|
+
/** Only trailing whitespace is cosmetic (indentation matters). */
|
|
34
|
+
indented?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const SLASH_BLOCK = [["/*", "*/"]] as const;
|
|
38
|
+
const C_LIKE: Syntax = { line: ["//"], block: SLASH_BLOCK, quotes: `"'` };
|
|
39
|
+
const SYNTAXES: Record<string, Syntax> = Object.fromEntries([
|
|
40
|
+
// Not jsx or tsx: JSX text (`don't`, `</a>`) is no lexable string or regex.
|
|
41
|
+
...["ts", "mts", "cts", "js", "mjs", "cjs"].map((ext) => [ext, { ...C_LIKE, js: true }]),
|
|
42
|
+
...["java", "kt", "kts", "scala", "swift", "dart", "groovy"].map((ext) => [
|
|
43
|
+
ext,
|
|
44
|
+
{ ...C_LIKE, triple: true },
|
|
45
|
+
]),
|
|
46
|
+
["go", { ...C_LIKE, rawBacktick: true }],
|
|
47
|
+
...["css", "scss", "less"].map((ext) => [
|
|
48
|
+
ext,
|
|
49
|
+
{ line: [], block: SLASH_BLOCK, quotes: `"'`, cssUrl: true },
|
|
50
|
+
]),
|
|
51
|
+
...["py", "pyi"].map((ext) => [
|
|
52
|
+
ext,
|
|
53
|
+
{ line: ["#"], block: [], quotes: `"'`, triple: true, indented: true, fstring: true },
|
|
54
|
+
]),
|
|
55
|
+
["toml", { line: ["#"], block: [], quotes: `"`, literal: "'", triple: true }],
|
|
56
|
+
...["html", "htm", "xml", "svg", "xhtml"].map((ext) => [
|
|
57
|
+
ext,
|
|
58
|
+
// Text and <pre> whitespace can matter: only trailing whitespace is cosmetic.
|
|
59
|
+
{ line: [], block: [["<!--", "-->"]], quotes: "", indented: true },
|
|
60
|
+
]),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/** Comments that steer a compiler, linter, bundler or runtime are code. */
|
|
64
|
+
const DIRECTIVE =
|
|
65
|
+
/@ts-|eslint|prettier|biome|oxlint|istanbul|\bc8\b|\bv8\b|@jsx|@flow|@vite|webpack|__PURE__|__NO_SIDE_EFFECTS__|source(?:Mapping)?URL|^\/\s*<|go:|\+build|nolint|noqa|type:|pyright|mypy|pylint|pragma|coding[:=]|fmt:|NOSONAR|@license|@preserve|@refresh|environment|language=|cython|ruff|isort|deno-|@vitest|@jest|swiftlint|ktlint|checkstyle|suppress|nullable|#(?:end)?region/iu;
|
|
66
|
+
|
|
67
|
+
const extensionOf = (file: string): string => {
|
|
68
|
+
const name = file.slice(file.lastIndexOf("/") + 1);
|
|
69
|
+
const dot = name.lastIndexOf(".");
|
|
70
|
+
return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** CI and workflow definitions are read as written: their comments are never stripped. */
|
|
74
|
+
const CI_PATH =
|
|
75
|
+
/^\.github\/|(?:^|\/)(?:\.gitlab-ci\.ya?ml|Jenkinsfile|azure-pipelines\.ya?ml|\.pre-commit-config\.ya?ml|\.circleci\/.*)$/u;
|
|
76
|
+
|
|
77
|
+
/** The syntax `file` is lexed with; null keeps comments and all but trailing whitespace. */
|
|
78
|
+
const syntaxOf = (file: string): Syntax | null =>
|
|
79
|
+
CI_PATH.test(file) ? null : (SYNTAXES[extensionOf(file)] ?? null);
|
|
80
|
+
|
|
81
|
+
const SPACE = /[ \t\r\f\v]/u;
|
|
82
|
+
const WORD = /[\w$]/u;
|
|
83
|
+
const NO_SPACE_AFTER = "([{,;";
|
|
84
|
+
const NO_SPACE_BEFORE = ")]},;";
|
|
85
|
+
const REGEX_AFTER = "(,=:[!&|?{};+-*%<>~^";
|
|
86
|
+
const REGEX_KEYWORDS = new Set([
|
|
87
|
+
"return",
|
|
88
|
+
"typeof",
|
|
89
|
+
"instanceof",
|
|
90
|
+
"case",
|
|
91
|
+
"do",
|
|
92
|
+
"else",
|
|
93
|
+
"in",
|
|
94
|
+
"of",
|
|
95
|
+
"new",
|
|
96
|
+
"delete",
|
|
97
|
+
"void",
|
|
98
|
+
"throw",
|
|
99
|
+
"yield",
|
|
100
|
+
"await",
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
type Frame = { kind: "code"; braces: number } | { kind: "template" };
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The substance of `text` for `file`: what stays after comments and
|
|
107
|
+
* insignificant whitespace are removed. Null when it cannot be read with
|
|
108
|
+
* certainty (the caller then counts the file as a behavior change).
|
|
109
|
+
*/
|
|
110
|
+
export function substanceOf(file: string, text: string): string | null {
|
|
111
|
+
const syntax = syntaxOf(file);
|
|
112
|
+
if (!syntax) return plainSubstance(text);
|
|
113
|
+
const markup = syntax.block.some(([open]) => open === "<!--");
|
|
114
|
+
// Markup with scripts, styles, CDATA or processing instructions holds
|
|
115
|
+
// other languages; a comment opener after anything but a tag end may sit
|
|
116
|
+
// in an attribute value.
|
|
117
|
+
if (
|
|
118
|
+
markup &&
|
|
119
|
+
(/<(?:script|style)\b|<!\[CDATA\[|<\?/iu.test(text) || /[^>\s][ \t]*<!--/u.test(text))
|
|
120
|
+
)
|
|
121
|
+
return plainSubstance(text);
|
|
122
|
+
return lex(syntax, text);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** No lexer: only trailing whitespace and blank lines are cosmetic. */
|
|
126
|
+
function plainSubstance(text: string): string {
|
|
127
|
+
return text
|
|
128
|
+
.split("\n")
|
|
129
|
+
.map((line) => line.replace(/[ \t\r\f\v]+$/u, ""))
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function lex(syntax: Syntax, text: string): string | null {
|
|
135
|
+
const collapse = !syntax.indented;
|
|
136
|
+
let out = "";
|
|
137
|
+
let lineStart = 0;
|
|
138
|
+
/** Output before this index is string content: trailing-space trimming stops there. */
|
|
139
|
+
let kept = 0;
|
|
140
|
+
let pendingSpace = false;
|
|
141
|
+
const frames: Frame[] = [{ kind: "code", braces: 0 }];
|
|
142
|
+
let index = 0;
|
|
143
|
+
|
|
144
|
+
// A shebang line is code, read as written.
|
|
145
|
+
if (text.startsWith("#!")) {
|
|
146
|
+
const end = text.indexOf("\n");
|
|
147
|
+
out = end < 0 ? text : text.slice(0, end);
|
|
148
|
+
kept = out.length;
|
|
149
|
+
index = end < 0 ? text.length : end;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const emitCode = (chunk: string) => {
|
|
153
|
+
if (collapse && pendingSpace && out.length > lineStart) {
|
|
154
|
+
const before = out[out.length - 1];
|
|
155
|
+
if (!NO_SPACE_AFTER.includes(before) && !NO_SPACE_BEFORE.includes(chunk[0])) out += " ";
|
|
156
|
+
}
|
|
157
|
+
pendingSpace = false;
|
|
158
|
+
out += chunk;
|
|
159
|
+
};
|
|
160
|
+
const emitVerbatim = (chunk: string) => {
|
|
161
|
+
emitCode(chunk);
|
|
162
|
+
kept = out.length;
|
|
163
|
+
};
|
|
164
|
+
const newline = () => {
|
|
165
|
+
if (!collapse) {
|
|
166
|
+
let end = out.length;
|
|
167
|
+
while (end > Math.max(lineStart, kept) && SPACE.test(out[end - 1])) end -= 1;
|
|
168
|
+
out = out.slice(0, end);
|
|
169
|
+
}
|
|
170
|
+
pendingSpace = false;
|
|
171
|
+
if (out.length > lineStart) {
|
|
172
|
+
out += "\n";
|
|
173
|
+
lineStart = out.length;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
/** Can a `/` start a regex here? After an operator or a keyword, not after a value. */
|
|
177
|
+
const regexAllowed = (): boolean => {
|
|
178
|
+
let end = out.length;
|
|
179
|
+
while (end > 0 && /\s/u.test(out[end - 1])) end -= 1;
|
|
180
|
+
if (end === 0 || REGEX_AFTER.includes(out[end - 1])) return true;
|
|
181
|
+
let start = end;
|
|
182
|
+
while (start > 0 && WORD.test(out[start - 1])) start -= 1;
|
|
183
|
+
return REGEX_KEYWORDS.has(out.slice(start, end));
|
|
184
|
+
};
|
|
185
|
+
/** Scan a quoted string from `index`; returns false when it does not close. */
|
|
186
|
+
const scanString = (quote: string, escapes: boolean, multiline: boolean): boolean => {
|
|
187
|
+
const prefix = text.slice(Math.max(0, index - 2), index).match(/[a-z]*$/iu)?.[0] ?? "";
|
|
188
|
+
const fstring = syntax.fstring === true && /f/iu.test(prefix);
|
|
189
|
+
let end = index + quote.length;
|
|
190
|
+
while (end < text.length) {
|
|
191
|
+
const char = text[end];
|
|
192
|
+
if (escapes && char === "\\") {
|
|
193
|
+
end += 2;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (text.startsWith(quote, end)) {
|
|
197
|
+
if (fstring && unbalanced(text.slice(index + quote.length, end))) return false;
|
|
198
|
+
emitVerbatim(text.slice(index, end + quote.length));
|
|
199
|
+
index = end + quote.length;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if (char === "\n" && !multiline) return false;
|
|
203
|
+
end += 1;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
while (index < text.length) {
|
|
209
|
+
const frame = frames[frames.length - 1];
|
|
210
|
+
const char = text[index];
|
|
211
|
+
if (frame.kind === "template") {
|
|
212
|
+
if (char === "\\") {
|
|
213
|
+
emitVerbatim(text.slice(index, index + 2));
|
|
214
|
+
index += 2;
|
|
215
|
+
} else if (char === "`") {
|
|
216
|
+
emitVerbatim(char);
|
|
217
|
+
frames.pop();
|
|
218
|
+
index += 1;
|
|
219
|
+
} else if (text.startsWith("${", index)) {
|
|
220
|
+
emitVerbatim("${");
|
|
221
|
+
frames.push({ kind: "code", braces: 0 });
|
|
222
|
+
index += 2;
|
|
223
|
+
} else {
|
|
224
|
+
// Template text, newlines included, is the string's value.
|
|
225
|
+
out += char;
|
|
226
|
+
kept = out.length;
|
|
227
|
+
index += 1;
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (char === "\n") {
|
|
232
|
+
newline();
|
|
233
|
+
index += 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (SPACE.test(char)) {
|
|
237
|
+
if (collapse) pendingSpace = true;
|
|
238
|
+
else out += char;
|
|
239
|
+
index += 1;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const lineOpener = syntax.line.find((opener) => text.startsWith(opener, index));
|
|
243
|
+
if (lineOpener) {
|
|
244
|
+
const end = text.indexOf("\n", index);
|
|
245
|
+
const comment = text.slice(index, end < 0 ? text.length : end);
|
|
246
|
+
if (DIRECTIVE.test(comment.slice(lineOpener.length))) emitVerbatim(comment);
|
|
247
|
+
else pendingSpace = true;
|
|
248
|
+
index = end < 0 ? text.length : end;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const block = syntax.block.find(([open]) => text.startsWith(open, index));
|
|
252
|
+
if (block) {
|
|
253
|
+
const end = text.indexOf(block[1], index + block[0].length);
|
|
254
|
+
if (end < 0) return null;
|
|
255
|
+
const comment = text.slice(index, end + block[1].length);
|
|
256
|
+
if (DIRECTIVE.test(comment.slice(block[0].length, -block[1].length))) emitVerbatim(comment);
|
|
257
|
+
else pendingSpace = true;
|
|
258
|
+
// A newline inside the comment still ends the code line before it.
|
|
259
|
+
if (comment.includes("\n")) newline();
|
|
260
|
+
index = end + block[1].length;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (syntax.triple && (char === '"' || char === "'") && text.startsWith(char.repeat(3), index)) {
|
|
264
|
+
if (!scanString(char.repeat(3), !syntax.literal?.includes(char), true)) return null;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (syntax.quotes.includes(char)) {
|
|
268
|
+
if (!scanString(char, true, Boolean(syntax.multiline))) return null;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (syntax.literal?.includes(char)) {
|
|
272
|
+
if (!scanString(char, false, false)) return null;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (syntax.rawBacktick && char === "`") {
|
|
276
|
+
if (!scanString("`", false, true)) return null;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (syntax.js && char === "`") {
|
|
280
|
+
emitVerbatim(char);
|
|
281
|
+
frames.push({ kind: "template" });
|
|
282
|
+
index += 1;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (syntax.cssUrl && /^url\(\s*[^"')\s]/iu.test(text.slice(index, index + 64))) return null;
|
|
286
|
+
if (syntax.js && char === "/" && !regexAllowed() && out.trimEnd().endsWith(")")) {
|
|
287
|
+
// Division after `)` may be a regex after `if (x)`: with another `/` on
|
|
288
|
+
// the line the two readings differ on what is code, so give up.
|
|
289
|
+
const lineEnd = text.indexOf("\n", index);
|
|
290
|
+
if (text.slice(index + 1, lineEnd < 0 ? text.length : lineEnd).includes("/")) return null;
|
|
291
|
+
}
|
|
292
|
+
if (syntax.js && char === "/" && regexAllowed()) {
|
|
293
|
+
let end = index + 1;
|
|
294
|
+
let inClass = false;
|
|
295
|
+
while (end < text.length && (inClass || text[end] !== "/")) {
|
|
296
|
+
if (text[end] === "\n") return null;
|
|
297
|
+
if (text[end] === "\\") end += 1;
|
|
298
|
+
else if (text[end] === "[") inClass = true;
|
|
299
|
+
else if (text[end] === "]") inClass = false;
|
|
300
|
+
end += 1;
|
|
301
|
+
}
|
|
302
|
+
if (end >= text.length) return null;
|
|
303
|
+
emitVerbatim(text.slice(index, end + 1));
|
|
304
|
+
index = end + 1;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (frames.length > 1 && frame.kind === "code") {
|
|
308
|
+
if (char === "{") frame.braces += 1;
|
|
309
|
+
else if (char === "}") {
|
|
310
|
+
if (frame.braces === 0) {
|
|
311
|
+
emitVerbatim(char);
|
|
312
|
+
frames.pop();
|
|
313
|
+
index += 1;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
frame.braces -= 1;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
emitCode(char);
|
|
320
|
+
index += 1;
|
|
321
|
+
}
|
|
322
|
+
if (frames.length !== 1) return null;
|
|
323
|
+
newline();
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Does an f-string body hold a `{` that never closes, once `{{` and `}}` are set aside? */
|
|
328
|
+
function unbalanced(body: string): boolean {
|
|
329
|
+
let depth = 0;
|
|
330
|
+
for (const part of body.replaceAll("{{", "").replaceAll("}}", "")) {
|
|
331
|
+
if (part === "{") depth += 1;
|
|
332
|
+
else if (part === "}") depth = Math.max(0, depth - 1);
|
|
333
|
+
}
|
|
334
|
+
return depth > 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* A lone CR (not followed by LF), U+2028 or U+2029: some readers end a line
|
|
339
|
+
* there and others do not, so a comment can hide code from one of them.
|
|
340
|
+
*/
|
|
341
|
+
export function hasOpaqueLineBreak(text: string): boolean {
|
|
342
|
+
return /\r(?!\n)|[\u2028\u2029]/u.test(text);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
346
|
+
|
|
347
|
+
/** The UTF-8 text of `bytes`, or null for binary (a NUL byte) or invalid UTF-8. */
|
|
348
|
+
export function textOf(bytes: Uint8Array): string | null {
|
|
349
|
+
if (bytes.includes(0)) return null;
|
|
350
|
+
try {
|
|
351
|
+
return decoder.decode(bytes);
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Is the change from `before` to `after` of `file` cosmetic (equal substance)? */
|
|
358
|
+
export function cosmeticChange(file: string, before: Uint8Array, after: Uint8Array): boolean {
|
|
359
|
+
const left = textOf(before);
|
|
360
|
+
const right = textOf(after);
|
|
361
|
+
if (left === null || right === null) return false;
|
|
362
|
+
if (hasOpaqueLineBreak(left) || hasOpaqueLineBreak(right)) return false;
|
|
363
|
+
const a = substanceOf(file, left);
|
|
364
|
+
return a !== null && a === substanceOf(file, right);
|
|
365
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalJson,
|
|
3
|
+
failure,
|
|
4
|
+
POLICY_VERSION,
|
|
5
|
+
policyChangeSchema,
|
|
6
|
+
policySchema,
|
|
7
|
+
sha256,
|
|
8
|
+
success,
|
|
9
|
+
type Constraint,
|
|
10
|
+
type Intent,
|
|
11
|
+
type Judgment,
|
|
12
|
+
type Policy,
|
|
13
|
+
type PolicyChange,
|
|
14
|
+
type Result,
|
|
15
|
+
} from "./task-contract";
|
|
16
|
+
import { deriveRequirements } from "./policy/derive";
|
|
17
|
+
import type { VerificationMode } from "../autonomy";
|
|
18
|
+
|
|
19
|
+
export type ResolverInput = {
|
|
20
|
+
intent: Intent;
|
|
21
|
+
judgment: Judgment;
|
|
22
|
+
constraints: Constraint[];
|
|
23
|
+
verification?: VerificationMode;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** The task policy for a judgment: derived requirements plus an input digest. */
|
|
27
|
+
export function resolvePolicy(input: ResolverInput): Result<Policy> {
|
|
28
|
+
const ids = input.constraints.map((constraint) => constraint.id);
|
|
29
|
+
if (new Set(ids).size !== ids.length)
|
|
30
|
+
return failure("invalid_input", "constraint IDs must be unique", {
|
|
31
|
+
fields: [{ path: "constraints", reason: "duplicate constraint id" }],
|
|
32
|
+
});
|
|
33
|
+
const policy: Policy = {
|
|
34
|
+
policyVersion: POLICY_VERSION,
|
|
35
|
+
inputDigest: sha256(
|
|
36
|
+
canonicalJson({
|
|
37
|
+
judgment: input.judgment,
|
|
38
|
+
scope: input.intent.scope,
|
|
39
|
+
constraints: input.constraints,
|
|
40
|
+
verification: input.verification ?? "self",
|
|
41
|
+
policyVersion: POLICY_VERSION,
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
requirements: deriveRequirements(
|
|
45
|
+
input.judgment,
|
|
46
|
+
input.intent.scope,
|
|
47
|
+
input.constraints,
|
|
48
|
+
input.verification,
|
|
49
|
+
),
|
|
50
|
+
};
|
|
51
|
+
const parsed = policySchema.safeParse(policy);
|
|
52
|
+
if (!parsed.success)
|
|
53
|
+
return failure("invalid_input", "resolved policy is invalid", {
|
|
54
|
+
fields: parsed.error.issues.map((issue) => ({
|
|
55
|
+
path: issue.path.join("."),
|
|
56
|
+
reason: issue.message,
|
|
57
|
+
})),
|
|
58
|
+
});
|
|
59
|
+
return success(null, null, parsed.data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const compareCodeUnits = (left: string, right: string): number =>
|
|
63
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
64
|
+
|
|
65
|
+
export function diffPolicy(
|
|
66
|
+
previous: Policy | null,
|
|
67
|
+
next: Policy,
|
|
68
|
+
reason: string,
|
|
69
|
+
now: string,
|
|
70
|
+
): PolicyChange | null {
|
|
71
|
+
if (!policySchema.safeParse(previous).success && previous !== null)
|
|
72
|
+
throw new TypeError("previous policy is invalid");
|
|
73
|
+
if (!policySchema.safeParse(next).success) throw new TypeError("next policy is invalid");
|
|
74
|
+
const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
|
|
75
|
+
const newIds = new Set(next.requirements.map((item) => item.id));
|
|
76
|
+
const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits);
|
|
77
|
+
const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits);
|
|
78
|
+
if (added.length === 0 && retired.length === 0) return null;
|
|
79
|
+
const normalizedReason = reason.trim();
|
|
80
|
+
const changeReason = retired.length
|
|
81
|
+
? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})`
|
|
82
|
+
: normalizedReason || "policy reassessed";
|
|
83
|
+
const change = {
|
|
84
|
+
recordedAt: now,
|
|
85
|
+
fromInputDigest: previous?.inputDigest ?? null,
|
|
86
|
+
toInputDigest: next.inputDigest,
|
|
87
|
+
added,
|
|
88
|
+
retired,
|
|
89
|
+
reason: changeReason,
|
|
90
|
+
} satisfies PolicyChange;
|
|
91
|
+
const parsed = policyChangeSchema.safeParse(change);
|
|
92
|
+
if (!parsed.success) throw new TypeError("policy change is invalid");
|
|
93
|
+
return parsed.data;
|
|
94
|
+
}
|