@mstar-harness/engine 0.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/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @mstar-harness/engine
2
+
3
+ Morning Star (启明星) harness engine — deterministic library for harness checks (version, path, status, lease, validation), shared by the installer CLI and the OpenCode plugin.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @mstar-harness/engine
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { readHarnessVersion } from "@mstar-harness/engine";
15
+
16
+ const version = readHarnessVersion(); // "1.8.8" — monorepo root package.json
17
+ ```
18
+
19
+ ## Scope
20
+
21
+ - Importable library only — **no `bin`**; the CLI (`@mstar-harness/cli`) wraps engine functions as thin `mstar …` subcommands.
22
+ - Dependencies: `node:*` only (zero external runtime deps — all validators hand-rolled; ajv pruned as phantom, zod removed 2026-08-08).
23
+ - Skill prose stays authoritative; engine exports are the machine-checkable mirror of the rules the `mstar-*` skills state.
24
+
25
+ ## License
26
+
27
+ MIT — see [LICENSE](https://github.com/btspoony/mstar-harness/blob/main/LICENSE).
@@ -0,0 +1,99 @@
1
+ import type { GateResult } from "./core.js";
2
+ /** Priority values (mstar-audit SKILL § Plan files Status block). */
3
+ export declare const AUDIT_PRIORITIES: readonly ["P1", "P2", "P3"];
4
+ export type AuditPriority = (typeof AUDIT_PRIORITIES)[number];
5
+ /** Effort values (Morning Star agent-oriented effort scale). */
6
+ export declare const AUDIT_EFFORTS: readonly ["XS", "S", "M", "L", "XL"];
7
+ export type AuditEffort = (typeof AUDIT_EFFORTS)[number];
8
+ /** Risk values. */
9
+ export declare const AUDIT_RISKS: readonly ["LOW", "MED", "HIGH"];
10
+ export type AuditRisk = (typeof AUDIT_RISKS)[number];
11
+ /** Category codes (finding-format.md § Category codes + SKILL Status block). */
12
+ export declare const AUDIT_CATEGORIES: readonly ["bug", "security", "perf", "tests", "tech-debt", "migration", "dx", "docs", "direction"];
13
+ export type AuditCategory = (typeof AUDIT_CATEGORIES)[number];
14
+ /**
15
+ * Validate the audit Status block(s) of a plan file against the field
16
+ * contract (mstar-audit SKILL § Plan files):
17
+ * - `Priority`: P1 | P2 | P3
18
+ * - `Effort`: XS | S | M | L | XL
19
+ * - `Risk`: LOW | MED | HIGH
20
+ * - `Depends on`: `none` or `plans/NNN-*.md` (the `*` is a literal
21
+ * wildcard form — the documented scaffolded scheme; concrete
22
+ * `plans/NNN-<slug>.md` paths are accepted too)
23
+ * - `Category`: bug | security | perf | tests | tech-debt | migration |
24
+ * dx | docs | direction
25
+ * - `Planned at`: `commit <short SHA>, <YYYY-MM-DD>` — `commit unknown`
26
+ * is accepted as the documented fallback (`scaffoldAuditPlan` default
27
+ * when the CLI runs outside a git repo)
28
+ *
29
+ * Every `## Status` block in the document is checked; a document without
30
+ * any block gets `audit.status.missing-block`. Violation codes:
31
+ * `audit.status.missing-block`, `audit.status.missing-field`,
32
+ * `audit.status.invalid-priority|effort|risk|depends-on|category|planned-at`.
33
+ */
34
+ export declare function validateAuditStatusBlocks(planText: string): GateResult;
35
+ /** A redacted credential occurrence: 1-based line + credential type. */
36
+ export type SecretFinding = {
37
+ line: number;
38
+ type: string;
39
+ };
40
+ /** Result of `redactSecrets`: redacted text + the findings summary. */
41
+ export type RedactResult = {
42
+ text: string;
43
+ findings: SecretFinding[];
44
+ };
45
+ /**
46
+ * Scan text for credential patterns and replace every occurrence with a
47
+ * `[REDACTED <type>@<line> in <file>]` marker (file omitted when `filePath`
48
+ * is not provided), per mstar-audit Hard Rule 4. Newlines are preserved so
49
+ * line numbers stay stable. Returns the redacted text plus a deduplicated,
50
+ * line-sorted findings summary (`{ line, type }`).
51
+ */
52
+ export declare function redactSecrets(text: string, filePath?: string): RedactResult;
53
+ /** One audit finding, shaped after finding-format.md. */
54
+ export type AuditFinding = {
55
+ title: string;
56
+ category: AuditCategory;
57
+ impact: string;
58
+ effort: AuditEffort;
59
+ risk: AuditRisk;
60
+ confidence: "HIGH" | "MED" | "LOW";
61
+ evidence: readonly string[];
62
+ priority: AuditPriority;
63
+ fixSketch?: string;
64
+ verification?: string;
65
+ dependsOn?: string;
66
+ };
67
+ /** Options for `scaffoldAuditPlan`. `plannedAt` defaults to the
68
+ * `repoShortSha` + `date`; `date` defaults to today (YYYY-MM-DD). */
69
+ export type ScaffoldAuditPlanOptions = {
70
+ date?: string;
71
+ repoName?: string;
72
+ repoShortSha?: string;
73
+ plannedAt?: {
74
+ commit: string;
75
+ date: string;
76
+ };
77
+ rejected?: readonly {
78
+ title: string;
79
+ reason: string;
80
+ }[];
81
+ };
82
+ /** Result of `scaffoldAuditPlan`. `nextNumber` is the next free plan number
83
+ * (monotonic continuation vs. any pre-existing `NNN-*.md` files). */
84
+ export type ScaffoldAuditPlanResult = {
85
+ outDir: string;
86
+ date: string;
87
+ files: string[];
88
+ nextNumber: number;
89
+ };
90
+ /**
91
+ * Scaffold an audit plan directory (`{PLAN_DIR}/audit-<date>/` layout,
92
+ * mstar-audit SKILL § Phase 4): numbered `NNN-<slug>.md` plan files from
93
+ * findings plus a README.md index. Numbering is monotonic — when the
94
+ * directory already contains `NNN-*.md` files (same-date re-run), the new
95
+ * batch continues after the highest existing number instead of restarting
96
+ * at 001, and the rebuilt index includes the pre-existing plans. Rejected
97
+ * findings render in the "considered and rejected" section.
98
+ */
99
+ export declare function scaffoldAuditPlan(outDir: string, findings: readonly AuditFinding[], options?: ScaffoldAuditPlanOptions): ScaffoldAuditPlanResult;
@@ -0,0 +1,90 @@
1
+ import type { GateResult } from "./core.js";
2
+ /** Required frontmatter fields (schema.yaml required_fields). */
3
+ export declare const KNOWLEDGE_REQUIRED_FIELDS: readonly ["module", "date", "problem_type", "category", "severity"];
4
+ /** All problem_type enum values (bug track + knowledge track). */
5
+ export declare const KNOWLEDGE_PROBLEM_TYPES: readonly ["build_error", "test_failure", "runtime_error", "performance_issue", "database_issue", "security_issue", "ui_bug", "integration_issue", "logic_error", "config_error", "developer_experience", "workflow_issue", "best_practice", "documentation_gap", "architecture_pattern", "design_pattern", "tooling_decision", "convention", "api_design", "testing_pattern"];
6
+ /** Bug-track problem_types (schema.yaml tracks.bug). */
7
+ export declare const KNOWLEDGE_BUG_PROBLEM_TYPES: readonly ["build_error", "test_failure", "runtime_error", "performance_issue", "database_issue", "security_issue", "ui_bug", "integration_issue", "logic_error", "config_error"];
8
+ /** Knowledge-track problem_types (schema.yaml tracks.knowledge). */
9
+ export declare const KNOWLEDGE_KNOWLEDGE_PROBLEM_TYPES: readonly ["developer_experience", "workflow_issue", "best_practice", "documentation_gap", "architecture_pattern", "design_pattern", "tooling_decision", "convention", "api_design", "testing_pattern"];
10
+ /** Severity enum (schema.yaml required_fields.severity). */
11
+ export declare const KNOWLEDGE_SEVERITIES: readonly ["critical", "high", "medium", "low"];
12
+ /** Bug-track resolution_type enum (schema.yaml track_rules.bug). */
13
+ export declare const KNOWLEDGE_RESOLUTION_TYPES: readonly ["code_fix", "migration", "config_change", "test_fix", "dependency_update", "environment_setup", "workflow_improvement", "documentation_update", "tooling_addition"];
14
+ /** problem_type → category directory (category-mapping.md; rule 1: the
15
+ * frontmatter `category` must match the mapped directory name). */
16
+ export declare const KNOWLEDGE_CATEGORY_MAP: Readonly<Record<string, string>>;
17
+ /**
18
+ * Validate a knowledge-doc frontmatter against the schema.yaml contract
19
+ * (embedded constants — no runtime skill-file reads):
20
+ * required fields (module, date, problem_type, category, severity), enum
21
+ * values, track rules (bug track needs symptoms / root_cause /
22
+ * resolution_type), category↔problem_type mapping consistency, and optional
23
+ * fields (plan_id, tags ≤ 8, last_updated, related_components).
24
+ *
25
+ * Violation codes:
26
+ * - `compound.schema.missing-frontmatter` — no `---` block
27
+ * - `compound.schema.missing-field` — required field absent/empty
28
+ * - `compound.schema.invalid-date` / `invalid-problem-type` /
29
+ * `invalid-severity` / `invalid-resolution-type`
30
+ * - `compound.schema.category-mismatch` — category ≠ mapping for
31
+ * problem_type (category-mapping.md rule 1)
32
+ * - `compound.schema.missing-track-field` — bug track missing
33
+ * symptoms/root_cause/resolution_type
34
+ * - `compound.schema.invalid-symptoms` / `invalid-root-cause` /
35
+ * `invalid-applies-when` / `invalid-plan-id` / `invalid-tags` /
36
+ * `tags-too-many` / `invalid-last-updated` / `invalid-related-components`
37
+ */
38
+ export declare function validateSchemaYaml(frontmatterText: string): GateResult;
39
+ /** Result of `referenceExists`: gate verdict + number of refs verified. */
40
+ export type ReferenceCheckResult = GateResult & {
41
+ checked: number;
42
+ };
43
+ /**
44
+ * Check that paths/functions referenced in a knowledge doc exist on disk
45
+ * (compound-refresh Phase 2 item 1: "Referenced code still exists?").
46
+ *
47
+ * Backticked refs are classified conservatively:
48
+ * - path-like refs (contain `/` or end in a known file extension) are
49
+ * resolved against `repoRoot` and must exist — `:line` suffixes and
50
+ * `#anchors` are stripped first; violation `compound.reference.missing-file`.
51
+ * - `module.symbol` refs use a documented heuristic: a module file named
52
+ * `<module>.ts|tsx|js|jsx|mjs|cjs` must exist somewhere under `repoRoot`;
53
+ * violation `compound.reference.module-missing` (low severity — heuristic).
54
+ * - URLs, `{PLACEHOLDER}` refs, globs, absolute paths, and bare symbols are
55
+ * skipped (not repo-relative, or not resolvable deterministically).
56
+ * `checked` counts unique refs that verified.
57
+ */
58
+ export declare function referenceExists(repoRoot: string, docText: string): ReferenceCheckResult;
59
+ /**
60
+ * Assert every knowledge doc under `knowledgeDir` has a row in
61
+ * `knowledgeDir/README.md` (mstar-compound SKILL.md Phase 6 index
62
+ * obligations). Row first cells may be plain paths or `[title](path)`
63
+ * links, with optional `./` / `knowledge/` prefixes. `README.md` and
64
+ * `index.md` files are not docs.
65
+ *
66
+ * Violation codes:
67
+ * - `compound.index.missing-readme` — no README.md index
68
+ * - `compound.index.missing-row` — doc with no index row
69
+ */
70
+ export declare function assertIndexRows(knowledgeDir: string): GateResult;
71
+ /**
72
+ * The compound-refresh scope (mstar-compound-refresh SKILL.md §
73
+ * 产物与操作路径): `{HARNESS_DIR}/knowledge/**`, `knowledge/README.md`,
74
+ * `<repo-root>/CONCEPTS.md`, `{HARNESS_DIR}/status.json`.
75
+ */
76
+ export declare function compoundRefreshScope(harnessDir: string, projectRoot: string): string[];
77
+ /**
78
+ * Guard an operation path against the allowed root set (compound-refresh
79
+ * scope SSOT: only knowledge/**, knowledge/README.md, CONCEPTS.md, and
80
+ * status.json may be written). File-like roots require an exact match;
81
+ * directory roots allow any path beneath them. `..` traversal-out is
82
+ * rejected via `resolve()` normalization.
83
+ *
84
+ * Limitation (documented): the guard is lexical — `resolve()` never follows
85
+ * symlinks, so a symlink inside an allowed root that points outside is not
86
+ * detected. Real enforcement is host-side (the sandbox/approval layer).
87
+ *
88
+ * Violation code: `compound.scope.outside` (medium).
89
+ */
90
+ export declare function scopeGuard(path: string, allowedRoots: readonly string[]): GateResult;
package/dist/core.d.ts ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Severity levels used across harness validation results.
3
+ *
4
+ * Machine SSOT — `mstar-plan-artifacts/references/status-and-residuals.md`
5
+ * § "Residual findings: `severity` (SSOT, machine field)" defines the same
6
+ * five lowercase-English values; `warning` / `Major` / any other value are
7
+ * forbidden in JSON severity fields.
8
+ */
9
+ export type Severity = "critical" | "high" | "medium" | "low" | "nit";
10
+ /**
11
+ * Total order, heavy → light (spec: status-and-residuals.md § severity):
12
+ * `critical` > `high` > `medium` > `low` > `nit`. `nit` is always lighter
13
+ * than `low` — never invert or equate.
14
+ */
15
+ export declare const SEVERITY_ORDER: readonly Severity[];
16
+ /**
17
+ * Result of one harness validation check.
18
+ *
19
+ * Shape per `.harness/references/skill-programmatic-roadmap.md` §8.5 C4:
20
+ * `{ ok: boolean, severity, code, message, fix? }`. v1 enforcement is
21
+ * non-blocking: callers (CLI output readers, host hooks) surface it as a
22
+ * warning, never a hard stop.
23
+ */
24
+ export type ValidationResult = {
25
+ ok: boolean;
26
+ severity: Severity;
27
+ code: string;
28
+ message: string;
29
+ fix?: string;
30
+ /**
31
+ * Backward-compat alias codes for the same violation, emitted by the
32
+ * engine's single parser (e.g. the Slice-2 `assignment.presence.*`
33
+ * namespace kept as aliases on the three core Assignment field
34
+ * violations — see `dispatch.requireField`). Consumers keyed off either
35
+ * namespace see exactly ONE violation per missing field.
36
+ */
37
+ aliases?: readonly string[];
38
+ };
39
+ /**
40
+ * Aggregate of validation checks for one gate (spec: roadmap §8.2 core row).
41
+ * `ok` is the gate verdict over `violations` — a gate with any violation
42
+ * does not pass.
43
+ *
44
+ * `hardBlocked` (Slice 5 / roadmap §8.5 C4 + D2) is the hard-enforcement
45
+ * overlay: `true` when the gate has violations AND the caller requested hard
46
+ * mode via `applyEnforcement`. Absent/`false` means warn-only — the caller
47
+ * may proceed with a warning. A caller that can refuse an action MUST refuse
48
+ * when `hardBlocked === true`.
49
+ */
50
+ export type GateResult = {
51
+ ok: boolean;
52
+ violations: ValidationResult[];
53
+ hardBlocked?: boolean;
54
+ };
55
+ /**
56
+ * Apply hard-enforcement semantics to a gate result (roadmap §8.5 C4/D2):
57
+ * `hardBlocked` is `true` exactly when `hard` is requested AND the gate has
58
+ * violations. `ok` and `violations` are preserved — enforcement is an
59
+ * overlay on the verdict, never a re-validation. When `hard` is false
60
+ * (flag absent/unset) the result is warn-only (`hardBlocked: false`), so
61
+ * rollback is simply unsetting the flag. Returns a NEW result; the input
62
+ * gate is not mutated.
63
+ */
64
+ export declare function applyEnforcement(gate: GateResult, opts: {
65
+ hard: boolean;
66
+ }): GateResult;
67
+ /**
68
+ * Read and parse a JSON document. Missing or empty files read as `{}`
69
+ * (same contract as the CLI helper this consolidates); malformed JSON
70
+ * throws with the file path in the message.
71
+ */
72
+ export declare function readJson(filePath: string): Record<string, unknown>;
73
+ /**
74
+ * Serialize `value` as pretty JSON with a trailing newline and write it
75
+ * atomically: temp file in the same directory, then rename over the target.
76
+ * Creates parent directories as needed; on failure the temp file is removed
77
+ * and the error rethrown, so the target is never partially written.
78
+ *
79
+ * Durability note (qc2 F-013): no fsync before the rename — atomicity (no
80
+ * partial file) is guaranteed by the same-dir temp + rename, but a power
81
+ * loss immediately after rename may lose the write. Acceptable for
82
+ * coordination files (status.json) whose writers re-read + verify the
83
+ * stored state; revisit if the harness moves to a filesystem without
84
+ * rename-atomicity guarantees.
85
+ */
86
+ export declare function writeJson(filePath: string, value: Record<string, unknown>): void;
87
+ /**
88
+ * Resolve the project root by walking up from `startDir` (default: cwd) to
89
+ * the nearest ancestor containing `package.json` or `bun.lock`. Falls back
90
+ * to the resolved `startDir` when no marker exists up to the filesystem root.
91
+ */
92
+ export declare function resolveProjectRoot(startDir?: string): string;
93
+ /**
94
+ * Resolve the harness version from a module directory: the module's OWN
95
+ * manifest first (`<moduleDir>/../package.json` — always shipped by npm,
96
+ * e.g. `node_modules/@mstar-harness/engine/package.json` next to
97
+ * `dist/engine.js`, or the CLI/opencode package.json next to their bundles),
98
+ * falling back to the monorepo root `morning-star` `package.json` walk.
99
+ * The single-version invariant makes both equivalent in-repo; the
100
+ * own-manifest-first order fixes published installs, where no
101
+ * `morning-star` manifest exists anywhere above `node_modules` and the walk
102
+ * alone would regress to `"0.0.0"` (qc3 F-1).
103
+ */
104
+ export declare function harnessVersionFrom(moduleDir: string): string;
105
+ /**
106
+ * Read the harness version (own-manifest first — see `harnessVersionFrom`).
107
+ *
108
+ * Single source for the harness version inside TS (roadmap §8.5 C6, moved
109
+ * from `packages/cli/src/utils.ts`); the CLI re-exports it unchanged. The
110
+ * single-version invariant keeps root, engine, cli and opencode aligned.
111
+ */
112
+ export declare function readHarnessVersion(): string;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Engine design-md module — DESIGN.md token frontmatter validation, light/dark
3
+ * parity, and completeness level audit.
4
+ *
5
+ * Spec sources (all embedded as constants — no runtime skill-file reads):
6
+ * - mstar-design-md SKILL.md: YAML frontmatter is the SSOT for token values;
7
+ * dual theme = same token names, different values; three completeness
8
+ * levels with LEVEL2/LEVEL3_PLACEHOLDER upgrade markers.
9
+ * - mstar-design-md/references/design-md-spec.md §1.5 (token store: colors /
10
+ * typography / spacing / rounded / components shapes), §3 (token naming),
11
+ * §4 (light/dark contract), §5 (upgrade placeholders), §6 ({path} refs).
12
+ * - mstar-design-md/references/completeness-checklist.md: Level 1–3 items;
13
+ * placeholders (`"[...]"`) never count as complete.
14
+ *
15
+ * Judgment stays prompt — this module is the deterministic half.
16
+ */
17
+ import type { GateResult } from "./core.js";
18
+ /** Parsed DESIGN.md frontmatter. Group values are `unknown` because a
19
+ * malformed group (e.g. `colors: red`) is a scalar; validation reports it. */
20
+ export type DesignFrontmatter = {
21
+ version?: string;
22
+ name?: string;
23
+ description?: string;
24
+ colors: Record<string, unknown>;
25
+ typography: Record<string, unknown>;
26
+ spacing: Record<string, unknown>;
27
+ rounded: Record<string, unknown>;
28
+ components: Record<string, unknown>;
29
+ };
30
+ /**
31
+ * Parse the leading `---`-fenced YAML frontmatter of a DESIGN.md into the
32
+ * five token groups plus version/name/description. Commented-out lines
33
+ * (including `# LEVEL2_PLACEHOLDER:` markers) are skipped, so only ACTIVE
34
+ * tokens count — matching the checklist rule that commented keys are not
35
+ * complete. Returns `null` when the text has no frontmatter block.
36
+ *
37
+ * A token group whose YAML value is a scalar is stored as
38
+ * `{ [RAW_GROUP]: <value> }` so validation can report `group-not-map`.
39
+ */
40
+ export declare function parseDesignFrontmatter(frontmatterText: string): DesignFrontmatter | null;
41
+ /**
42
+ * Validate a DESIGN.md frontmatter against the token-store contract
43
+ * (design-md-spec §1.5): the colors/typography/spacing/rounded groups must
44
+ * exist with spec-typed values, `components` (optional below Level 2) must
45
+ * carry `{path}` references that resolve within the same file.
46
+ *
47
+ * Violation codes:
48
+ * - `design-md.tokens.missing-frontmatter` — no `---` block
49
+ * - `design-md.tokens.missing-group` — required group absent/empty
50
+ * - `design-md.tokens.group-not-map` — group value is a scalar
51
+ * - `design-md.tokens.color-format` — not hex or oklch()
52
+ * - `design-md.tokens.typography-shape` — not exactly the five properties
53
+ * - `design-md.tokens.spacing-base` / `spacing-key` / `spacing-format`
54
+ * - `design-md.tokens.rounded-format` — not a px length
55
+ * - `design-md.tokens.components-shape` — component entry not a map
56
+ * - `design-md.tokens.ref-unresolved` — `{group.key}` does not resolve
57
+ * - `design-md.tokens.placeholder` — `"[...]"` template value (low)
58
+ */
59
+ export declare function validateDesignTokenFrontmatter(frontmatterText: string): GateResult;
60
+ /**
61
+ * Assert the light/dark dual-theme contract (design-md-spec §4 rules 1–4):
62
+ * both files define the SAME token key set across all five groups; only
63
+ * values differ. Every token active in one file must be active in the other.
64
+ *
65
+ * Violation codes:
66
+ * - `design-md.parity.missing-frontmatter` — either file has no block
67
+ * - `design-md.parity.missing-dark` — token present in light only
68
+ * - `design-md.parity.missing-light` — token present in dark only
69
+ */
70
+ export declare function assertLightDarkParity(lightFm: string, darkFm: string): GateResult;
71
+ /** Completeness level verdict (checklist § Verdict). */
72
+ export type CompletenessLevel = "BELOW_MVP" | "MVP" | "Standard" | "Production";
73
+ /** One checklist item: id, owning level, source, and pass/fail. */
74
+ export type CompletenessItem = {
75
+ id: string;
76
+ level: 1 | 2 | 3;
77
+ ok: boolean;
78
+ source: "frontmatter" | "body";
79
+ };
80
+ /** A `LEVEL2_PLACEHOLDER` / `LEVEL3_PLACEHOLDER` marker found in the text. */
81
+ export type CompletenessPlaceholder = {
82
+ level: 2 | 3;
83
+ marker: string;
84
+ line: number;
85
+ };
86
+ /** Result of `completenessLevel`. `bodyUnverified` is true when the caller
87
+ * passed no checklist (body-only items were excluded from the level
88
+ * computation and Production is capped at Standard). */
89
+ export type CompletenessResult = {
90
+ level: CompletenessLevel;
91
+ items: CompletenessItem[];
92
+ /** Ids failing at the next-highest level boundary (or the current level
93
+ * when below MVP). */
94
+ missing: string[];
95
+ placeholders: CompletenessPlaceholder[];
96
+ upgradeTo: 2 | 3 | null;
97
+ bodyUnverified: boolean;
98
+ };
99
+ /**
100
+ * Determine the DESIGN.md completeness level from its frontmatter
101
+ * (completeness-checklist.md): Level 1 (MVP) → Level 2 (Standard) → Level 3
102
+ * (Production). A level is complete only when all items at that level and
103
+ * below pass; placeholder values and commented-out keys never count.
104
+ *
105
+ * Body-only items (breakpoints, elevation, motion, voice, dark-file) cannot
106
+ * be verified from frontmatter — pass their ids in `checklist` to confirm
107
+ * them. When `checklist` is omitted, body items are excluded from level
108
+ * computation and Production is capped at Standard (`bodyUnverified: true`).
109
+ *
110
+ * Placeholder detection scans for `LEVEL2_PLACEHOLDER` / `LEVEL3_PLACEHOLDER`
111
+ * markers (design-md-spec §5) and suggests the implied upgrade target.
112
+ */
113
+ export declare function completenessLevel(frontmatterText: string, checklist?: readonly string[]): CompletenessResult;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Engine dispatch module — Assignment field validation, default-branch gate,
3
+ * execution-mode → QC seat count, tri identity, anti-recursion precheck.
4
+ *
5
+ * Spec sources (semantic SSOT — the skills stay authoritative; this module
6
+ * implements their deterministic rules without forking semantics):
7
+ * - Assignment field contract (`Execute as` / `Delegation` / `Task category`
8
+ * present, non-empty; paste-only assignments missing fields are flagged):
9
+ * `mstar-dispatch-gates` SKILL.md § "调度防串扰(强制)" + § 反模式(派发)
10
+ * ("Assignment 已写、invoke 为零(paste-only)").
11
+ * - Branch-field exactly-one rule + `<base>` requirement: `mstar-branch-worktree`
12
+ * SKILL.md § "Assignment 要求(PM)" + § "`<base>` 与叠分支(stacked
13
+ * branches)" ("若写新建但未写 `<base>`:实现侧应停下问 project-manager…
14
+ * 禁止擅自假设「一定是 main」").
15
+ * - Default-protected-branch gate (`main`/`master` unless an explicit
16
+ * `Branch policy: direct on <branch> — <reason>` exception exists):
17
+ * `mstar-branch-worktree` SKILL.md § "Git 功能分支门禁(业务仓库)".
18
+ * - N→seat mapping (sdd→3 tri, inline→1 single, targeted→listed seats) and
19
+ * tri identity (`qc-specialist` / `qc-specialist-2` / `qc-specialist-3`):
20
+ * `mstar-dispatch-gates` SKILL.md § "QC tri-review(SDD 强制)" / "QC
21
+ * 单席(例外)" / "QC targeted re-review" + `mstar-roles` SKILL.md
22
+ * "QC reviewer" 参数表.
23
+ * - Anti-recursion NEVER red line (role binding == `Execute as`): `mstar-dispatch-gates`
24
+ * SKILL.md § "承接方反递归红线(NEVER / DO NOT;leaf executor 必读)".
25
+ * - Hard-gate enforcement (`Enforcement: hard` flag — per Assignment/compass,
26
+ * never global; rollback = unset flag): `.harness/references/skill-programmatic-roadmap.md`
27
+ * §8.5 C4 + decision D2.
28
+ */
29
+ import type { GateResult } from "./core.js";
30
+ /** Parsed Assignment header fields relevant to dispatch validation. */
31
+ export type AssignmentFields = {
32
+ executeAs?: string;
33
+ delegation?: string;
34
+ taskCategory?: string;
35
+ workingBranch?: string;
36
+ branchPolicy?: string;
37
+ };
38
+ export type ValidateAssignmentFieldsOptions = {
39
+ /**
40
+ * Whether the assignment produces repo diffs (default `true`). The
41
+ * branch-form exactly-one gate applies to writable assignments only —
42
+ * read-only assignments (explore/scout orientation) legitimately omit
43
+ * branch fields per mstar-branch-worktree ("每个可写 Assignment…").
44
+ */
45
+ writable?: boolean;
46
+ };
47
+ /** Options for {@link assertDefaultBranchProtected}. */
48
+ export type DefaultBranchOptions = {
49
+ /** Protected default branch names (project convention; default `main`/`master`). */
50
+ defaultBranches?: readonly string[];
51
+ /** True when the Assignment carries an explicit `Branch policy: direct on …` exception. */
52
+ directOnException?: boolean;
53
+ };
54
+ /** Options for {@link executionModeToN}. */
55
+ export type ExecutionModeToNOptions = {
56
+ /** Listed reviewer seats for `targeted` re-review (`QC re-review: targeted — reviewers: …`). */
57
+ seats?: readonly string[];
58
+ };
59
+ /** Result of {@link executionModeToN}: a GateResult carrying `n` on success. */
60
+ export type ExecutionModeToNResult = GateResult & {
61
+ n?: number;
62
+ };
63
+ /**
64
+ * Parse `**Field**: value` (or plain `Field: value`) header lines from an
65
+ * Assignment into the dispatch-relevant fields. Only known field labels are
66
+ * captured; values are trimmed. List-bullet prefixes (`- **Field**: value`)
67
+ * are accepted so the engine parser is the SINGLE grammar for Assignment
68
+ * header fields (the Slice-2 opencode presence parser tolerated bullets;
69
+ * its acceptance is folded into this parser, not forked — qc1 F-002).
70
+ */
71
+ export declare function parseAssignmentFields(assignmentText: string): AssignmentFields;
72
+ /** Where the `Enforcement` flag was declared (roadmap §8.5 C4/D2). */
73
+ export type EnforcementSource = "assignment" | "compass" | "none";
74
+ /** Parsed hard-enforcement flag. `hard: false` + `source: none` = flag absent. */
75
+ export type EnforcementFlag = {
76
+ hard: boolean;
77
+ source: EnforcementSource;
78
+ };
79
+ /**
80
+ * Slice an Assignment's header region — the text before the first body
81
+ * marker (see {@link ASSIGNMENT_BODY_START_RE}). Returns the full text when
82
+ * no marker is present. The Assignment enforcement flag is parsed against
83
+ * THIS region only, so an example line `**Enforcement**: hard` quoted in the
84
+ * task body cannot harden the dispatch (qc1 F-003 / qc2 F-003).
85
+ */
86
+ export declare function assignmentHeaderRegion(assignmentText: string): string;
87
+ /**
88
+ * Parse the `Enforcement: hard` flag (roadmap §8.5 C4 + decision D2 — v2
89
+ * hard gates are OPT-IN per Assignment/compass, never global; rollback =
90
+ * unset flag; inert when the engine is absent).
91
+ *
92
+ * Recognized forms, checked in order:
93
+ * 1. Assignment header `**Enforcement**: hard` / `Enforcement: hard`
94
+ * (bold or plain, optional list bullet; value case-insensitive).
95
+ * 2. Compass frontmatter YAML key `enforcement: hard` (lowercase key;
96
+ * value may be quoted, case-insensitive).
97
+ *
98
+ * The Assignment form wins over the compass form when both appear in the
99
+ * input (per-Assignment precedence — a dispatch's own flag is decisive).
100
+ * A present-but-non-hard value (`soft`, empty, malformed) still reports
101
+ * its source so callers can distinguish "explicitly not hard" from
102
+ * "not mentioned" (`source: none`). Never throws.
103
+ *
104
+ * Assignment-form callers MUST pass the header region (see
105
+ * {@link assignmentHeaderRegion}) — this function itself scans the whole
106
+ * input because the compass form is fed raw frontmatter, which has no
107
+ * body markers (qc1 F-003 / qc2 F-003).
108
+ */
109
+ export declare function parseEnforcementFlag(text: string): EnforcementFlag;
110
+ /**
111
+ * Parsed branch forms of an Assignment (mstar-branch-worktree § "Assignment
112
+ * 要求"): `Working branch: <existing>` | `Working branch: create <new> from
113
+ * <base>` | `Branch policy: direct on <branch> — <reason>`. Exactly one is
114
+ * required for writable assignments. This is the engine's SINGLE branch-form
115
+ * grammar — CLI and host hooks consume it instead of re-implementing the
116
+ * regexes (qc1 F-001 / qc3 F-3).
117
+ */
118
+ export type AssignmentBranchForms = {
119
+ /**
120
+ * `Working branch: <existing>` — the value's first token (create-form
121
+ * values are excluded and land in {@link createForm} instead).
122
+ */
123
+ workingBranch?: string;
124
+ /** `Working branch: create <new> from <base>` — created branch name (+ base when written). */
125
+ createForm?: {
126
+ name: string;
127
+ base?: string;
128
+ };
129
+ /**
130
+ * `Branch policy: direct on <branch> — <reason>` — branch captured by the
131
+ * loose `direct on <branch>` prefix; `reason` is the strict-form reason
132
+ * ("" when the value is not a well-formed direct-on form, i.e. no
133
+ * separator + non-empty reason — mirror of `validateAssignmentFields`).
134
+ */
135
+ directOn?: {
136
+ branch: string;
137
+ reason: string;
138
+ };
139
+ };
140
+ /**
141
+ * Parse an Assignment's branch forms via the engine's single parser
142
+ * (`parseAssignmentFields` + {@link parseWorkingBranchValue}). Consumed by
143
+ * the CLI `dispatch validate` gate-branch derivation and the opencode hook;
144
+ * also the internal grammar behind `validateAssignmentFields`.
145
+ */
146
+ export declare function parseAssignmentBranchForms(assignmentText: string): AssignmentBranchForms;
147
+ /**
148
+ * Parse the Assignment's `Branch policy: direct on <branch> — <reason>`
149
+ * exception branch. Returns the branch ONLY for the well-formed direct-on
150
+ * form (branch + non-empty reason; separator set [—–]|--|-); undefined when
151
+ * absent or malformed — the default-branch gate recognizes explicit
152
+ * direct-on exceptions only. Single engine grammar shared by CLI + plugin
153
+ * (qc1 F-001).
154
+ */
155
+ export declare function parseBranchPolicyDirectOnBranch(assignmentText: string): string | undefined;
156
+ /**
157
+ * True when the Assignment's `Execute as` role is a read-only orientation
158
+ * role (`scout` / `explore`, case-insensitive). Read-only assignments
159
+ * legitimately omit branch forms (mstar-branch-worktree § "每个可写
160
+ * Assignment…") — callers pass `validateAssignmentFields(text, { writable:
161
+ * false })` and skip the default-branch gate for them (qc3 F-1 / qc2 S-5).
162
+ */
163
+ export declare function isReadOnlyAssignmentRole(roleId: string): boolean;
164
+ /**
165
+ * Validate an Assignment's header fields (mstar-dispatch-gates Assignment
166
+ * field contract + mstar-branch-worktree branch-form contract).
167
+ *
168
+ * Required: `Execute as` / `Delegation` / `Task category` present with
169
+ * non-empty values (paste-only shells are caught here — every field missing).
170
+ * Writable assignments must carry EXACTLY ONE branch form; `create <new>
171
+ * from <base>` without `<base>` (incl. the dangling `create <new> from`
172
+ * / `create from <base>` typos) and `Branch policy` without branch/reason
173
+ * are flagged. The three core-field violations carry the legacy
174
+ * `assignment.presence.*` codes as aliases (qc1 F-002).
175
+ */
176
+ export declare function validateAssignmentFields(assignmentText: string, opts?: ValidateAssignmentFieldsOptions): GateResult;
177
+ /**
178
+ * Flag writable work on a default protected branch (`main`/`master` per
179
+ * project convention) unless an explicit direct-on exception is present
180
+ * (mstar-branch-worktree § "Git 功能分支门禁"). `directOnException` mirrors
181
+ * the Assignment carrying `Branch policy: direct on <branch> — <reason>`.
182
+ */
183
+ export declare function assertDefaultBranchProtected(branch: string, opts?: DefaultBranchOptions): GateResult;
184
+ /**
185
+ * Map an Assignment `Execution mode` to its QC seat count N
186
+ * (mstar-dispatch-gates § QC tri / 单席 / targeted): `sdd` → 3 (tri),
187
+ * `inline` → 1, `targeted` → the listed reviewer seats. Unknown or missing
188
+ * modes are violations.
189
+ */
190
+ export declare function executionModeToN(executionMode: string, opts?: ExecutionModeToNOptions): ExecutionModeToNResult;
191
+ /**
192
+ * Assert the initial QC wave's reviewer roles are exactly
193
+ * `qc-specialist` / `qc-specialist-2` / `qc-specialist-3`
194
+ * (mstar-dispatch-gates § QC tri-review; mstar-roles QC reviewer 参数表).
195
+ * Any other composition — missing seat, duplicate, or foreign role — fails.
196
+ */
197
+ export declare function assertTriIdentity(reviewerRoles: readonly string[]): GateResult;
198
+ /**
199
+ * Anti-recursion precheck (NEVER red line, mstar-dispatch-gates § 承接方反递归
200
+ * 红线): a leaf executor MUST NOT invoke a Task/subagent whose role-binding
201
+ * field (`subagent_type` / `agent` / `subagent`) equals its own `Execute as`.
202
+ * Comparison is case-insensitive after trim; an empty binding is not a
203
+ * self-recursion (its presence is the field gate's job, not this precheck).
204
+ */
205
+ export declare function antiRecursionPrecheck(subagentType: string, executeAs: string): GateResult;