@prd-improve/cli 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/README.md +38 -0
- package/dist/anchor-slicer.d.ts +18 -0
- package/dist/anchor-slicer.js +114 -0
- package/dist/anchor-slicer.js.map +1 -0
- package/dist/api/client.d.ts +68 -0
- package/dist/api/client.js +175 -0
- package/dist/api/client.js.map +1 -0
- package/dist/changelog.d.ts +9 -0
- package/dist/changelog.js +34 -0
- package/dist/changelog.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +258 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/admin/_adminJson.d.ts +18 -0
- package/dist/commands/admin/_adminJson.js +27 -0
- package/dist/commands/admin/_adminJson.js.map +1 -0
- package/dist/commands/admin/_authResolve.d.ts +8 -0
- package/dist/commands/admin/_authResolve.js +56 -0
- package/dist/commands/admin/_authResolve.js.map +1 -0
- package/dist/commands/admin/index.d.ts +22 -0
- package/dist/commands/admin/index.js +100 -0
- package/dist/commands/admin/index.js.map +1 -0
- package/dist/commands/admin/io.d.ts +15 -0
- package/dist/commands/admin/io.js +30 -0
- package/dist/commands/admin/io.js.map +1 -0
- package/dist/commands/admin/member.d.ts +43 -0
- package/dist/commands/admin/member.js +362 -0
- package/dist/commands/admin/member.js.map +1 -0
- package/dist/commands/admin/project.d.ts +40 -0
- package/dist/commands/admin/project.js +182 -0
- package/dist/commands/admin/project.js.map +1 -0
- package/dist/commands/admin/team.d.ts +31 -0
- package/dist/commands/admin/team.js +157 -0
- package/dist/commands/admin/team.js.map +1 -0
- package/dist/commands/admin/teamMember.d.ts +33 -0
- package/dist/commands/admin/teamMember.js +262 -0
- package/dist/commands/admin/teamMember.js.map +1 -0
- package/dist/commands/admin/token.d.ts +29 -0
- package/dist/commands/admin/token.js +125 -0
- package/dist/commands/admin/token.js.map +1 -0
- package/dist/commands/admin/user.d.ts +2 -0
- package/dist/commands/admin/user.js +85 -0
- package/dist/commands/admin/user.js.map +1 -0
- package/dist/commands/config.d.ts +9 -0
- package/dist/commands/config.js +178 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/context.d.ts +11 -0
- package/dist/commands/context.js +235 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/diff.d.ts +6 -0
- package/dist/commands/diff.js +208 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/freeze.d.ts +10 -0
- package/dist/commands/freeze.js +269 -0
- package/dist/commands/freeze.js.map +1 -0
- package/dist/commands/init.d.ts +24 -0
- package/dist/commands/init.js +229 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +26 -0
- package/dist/commands/login.js +241 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +6 -0
- package/dist/commands/logout.js +75 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/migrate-yaml.d.ts +17 -0
- package/dist/commands/migrate-yaml.js +288 -0
- package/dist/commands/migrate-yaml.js.map +1 -0
- package/dist/commands/project.d.ts +7 -0
- package/dist/commands/project.js +87 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/pull.d.ts +16 -0
- package/dist/commands/pull.js +197 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/push.d.ts +9 -0
- package/dist/commands/push.js +392 -0
- package/dist/commands/push.js.map +1 -0
- package/dist/commands/refresh.d.ts +9 -0
- package/dist/commands/refresh.js +123 -0
- package/dist/commands/refresh.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +190 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/team.d.ts +6 -0
- package/dist/commands/team.js +84 -0
- package/dist/commands/team.js.map +1 -0
- package/dist/commands/validate.d.ts +5 -0
- package/dist/commands/validate.js +129 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/whoami.d.ts +6 -0
- package/dist/commands/whoami.js +81 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config/adminBackend.d.ts +43 -0
- package/dist/config/adminBackend.js +33 -0
- package/dist/config/adminBackend.js.map +1 -0
- package/dist/config/rc.d.ts +70 -0
- package/dist/config/rc.js +148 -0
- package/dist/config/rc.js.map +1 -0
- package/dist/config/schema.d.ts +132 -0
- package/dist/config/schema.js +36 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/diff/engine.d.ts +35 -0
- package/dist/diff/engine.js +305 -0
- package/dist/diff/engine.js.map +1 -0
- package/dist/diff/render.d.ts +18 -0
- package/dist/diff/render.js +129 -0
- package/dist/diff/render.js.map +1 -0
- package/dist/formatter/colors.d.ts +17 -0
- package/dist/formatter/colors.js +35 -0
- package/dist/formatter/colors.js.map +1 -0
- package/dist/formatter/human.d.ts +7 -0
- package/dist/formatter/human.js +70 -0
- package/dist/formatter/human.js.map +1 -0
- package/dist/fs/atomicWrite.d.ts +7 -0
- package/dist/fs/atomicWrite.js +37 -0
- package/dist/fs/atomicWrite.js.map +1 -0
- package/dist/i18n/dispatch.d.ts +19 -0
- package/dist/i18n/dispatch.js +27 -0
- package/dist/i18n/dispatch.js.map +1 -0
- package/dist/i18n/en.d.ts +3 -0
- package/dist/i18n/en.js +275 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/help.d.ts +3 -0
- package/dist/i18n/help.en.d.ts +149 -0
- package/dist/i18n/help.en.js +150 -0
- package/dist/i18n/help.en.js.map +1 -0
- package/dist/i18n/help.js +7 -0
- package/dist/i18n/help.js.map +1 -0
- package/dist/i18n/help.zh.d.ts +149 -0
- package/dist/i18n/help.zh.js +150 -0
- package/dist/i18n/help.zh.js.map +1 -0
- package/dist/i18n/helpFormat.d.ts +7 -0
- package/dist/i18n/helpFormat.js +51 -0
- package/dist/i18n/helpFormat.js.map +1 -0
- package/dist/i18n/helpGroups.d.ts +3 -0
- package/dist/i18n/helpGroups.js +11 -0
- package/dist/i18n/helpGroups.js.map +1 -0
- package/dist/i18n/index.d.ts +16 -0
- package/dist/i18n/index.js +46 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/peekLang.d.ts +1 -0
- package/dist/i18n/peekLang.js +16 -0
- package/dist/i18n/peekLang.js.map +1 -0
- package/dist/i18n/zh.d.ts +680 -0
- package/dist/i18n/zh.js +288 -0
- package/dist/i18n/zh.js.map +1 -0
- package/dist/parser/discover.d.ts +2 -0
- package/dist/parser/discover.js +69 -0
- package/dist/parser/discover.js.map +1 -0
- package/dist/parser/load-yaml.d.ts +11 -0
- package/dist/parser/load-yaml.js +25 -0
- package/dist/parser/load-yaml.js.map +1 -0
- package/dist/pkgVersion.d.ts +9 -0
- package/dist/pkgVersion.js +22 -0
- package/dist/pkgVersion.js.map +1 -0
- package/dist/renderer/context-md.d.ts +27 -0
- package/dist/renderer/context-md.js +444 -0
- package/dist/renderer/context-md.js.map +1 -0
- package/dist/renderer/refresh-md.d.ts +18 -0
- package/dist/renderer/refresh-md.js +93 -0
- package/dist/renderer/refresh-md.js.map +1 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/modules.d.ts +461 -0
- package/dist/schema/modules.js +184 -0
- package/dist/schema/modules.js.map +1 -0
- package/dist/schema/prd.d.ts +1018 -0
- package/dist/schema/prd.js +65 -0
- package/dist/schema/prd.js.map +1 -0
- package/dist/sync/cliRcHelpers.d.ts +25 -0
- package/dist/sync/cliRcHelpers.js +62 -0
- package/dist/sync/cliRcHelpers.js.map +1 -0
- package/dist/sync/conflicts.d.ts +7 -0
- package/dist/sync/conflicts.js +72 -0
- package/dist/sync/conflicts.js.map +1 -0
- package/dist/sync/headProbe.d.ts +1 -0
- package/dist/sync/headProbe.js +95 -0
- package/dist/sync/headProbe.js.map +1 -0
- package/dist/sync/normalize.d.ts +10 -0
- package/dist/sync/normalize.js +13 -0
- package/dist/sync/normalize.js.map +1 -0
- package/dist/sync/syncJson.d.ts +18 -0
- package/dist/sync/syncJson.js +42 -0
- package/dist/sync/syncJson.js.map +1 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/dist/validators/blocking.d.ts +2 -0
- package/dist/validators/blocking.js +25 -0
- package/dist/validators/blocking.js.map +1 -0
- package/dist/validators/duplicate-ids.d.ts +2 -0
- package/dist/validators/duplicate-ids.js +58 -0
- package/dist/validators/duplicate-ids.js.map +1 -0
- package/dist/validators/references.d.ts +5 -0
- package/dist/validators/references.js +165 -0
- package/dist/validators/references.js.map +1 -0
- package/dist/validators/required-modules.d.ts +8 -0
- package/dist/validators/required-modules.js +59 -0
- package/dist/validators/required-modules.js.map +1 -0
- package/dist/validators/runner.d.ts +11 -0
- package/dist/validators/runner.js +73 -0
- package/dist/validators/runner.js.map +1 -0
- package/dist/workspace.d.ts +32 -0
- package/dist/workspace.js +37 -0
- package/dist/workspace.js.map +1 -0
- package/dist/yaml-indexer.d.ts +15 -0
- package/dist/yaml-indexer.js +112 -0
- package/dist/yaml-indexer.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { metaSchema, backgroundAndGoalsSchema, userRoleSchema, inScopeItemSchema, outOfScopeItemSchema, entitySchema, mainFlowStepSchema, statePermissionSchema, businessRuleSchema, scenarioSchema, edgeCaseSchema, uiPageSchema, dataAndApiSchema, acceptanceCriterionSchema, openQuestionSchema, slugPattern, } from "./modules.js";
|
|
3
|
+
/**
|
|
4
|
+
* 当前 schema 版本(v1.1 起 yaml meta 必带 team / project / feature 三 slug)。
|
|
5
|
+
* 仍接受 v0.1 以支持 `prd migrate-yaml` 升级前的存量 yaml(spec §3.2 backward compat)。
|
|
6
|
+
*/
|
|
7
|
+
export const SCHEMA_VERSION_CURRENT = "v0.2";
|
|
8
|
+
export const SCHEMA_VERSION_LEGACY = "v0.1";
|
|
9
|
+
export const PrdSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
schema_version: z.enum([SCHEMA_VERSION_LEGACY, SCHEMA_VERSION_CURRENT]),
|
|
12
|
+
meta: metaSchema,
|
|
13
|
+
background_and_goals: backgroundAndGoalsSchema,
|
|
14
|
+
users_and_roles: z.array(userRoleSchema),
|
|
15
|
+
in_scope: z.array(inScopeItemSchema),
|
|
16
|
+
out_of_scope: z.array(outOfScopeItemSchema),
|
|
17
|
+
entities: z.array(entitySchema),
|
|
18
|
+
main_flow: z.array(mainFlowStepSchema),
|
|
19
|
+
state_permissions: z.array(statePermissionSchema),
|
|
20
|
+
business_rules: z.array(businessRuleSchema),
|
|
21
|
+
scenarios: z.array(scenarioSchema),
|
|
22
|
+
edge_cases: z.array(edgeCaseSchema),
|
|
23
|
+
ui_interaction: z.array(uiPageSchema),
|
|
24
|
+
data_and_api: dataAndApiSchema,
|
|
25
|
+
acceptance_criteria: z.array(acceptanceCriterionSchema),
|
|
26
|
+
open_questions: z.array(openQuestionSchema),
|
|
27
|
+
})
|
|
28
|
+
.strict()
|
|
29
|
+
.superRefine((doc, ctx) => {
|
|
30
|
+
// v0.1 keeps the legacy free-form meta(向后兼容);v0.2 起强制 slug.
|
|
31
|
+
if (doc.schema_version !== SCHEMA_VERSION_CURRENT)
|
|
32
|
+
return;
|
|
33
|
+
const meta = doc.meta;
|
|
34
|
+
// team 必填 + 必须是 slug
|
|
35
|
+
if (meta.team === undefined || meta.team === null || meta.team === "") {
|
|
36
|
+
ctx.addIssue({
|
|
37
|
+
code: z.ZodIssueCode.custom,
|
|
38
|
+
path: ["meta", "team"],
|
|
39
|
+
message: "v0.2 yaml 必须包含 meta.team(kebab-case slug)",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else if (!slugPattern.test(meta.team)) {
|
|
43
|
+
ctx.addIssue({
|
|
44
|
+
code: z.ZodIssueCode.custom,
|
|
45
|
+
path: ["meta", "team"],
|
|
46
|
+
message: "meta.team 必须 kebab-case slug:首字母 a-z,只含 a-z 0-9 -,长度 2-64",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// project / feature v0.1 已必填(zod 类型),v0.2 加 slug 校验
|
|
50
|
+
if (!slugPattern.test(meta.project)) {
|
|
51
|
+
ctx.addIssue({
|
|
52
|
+
code: z.ZodIssueCode.custom,
|
|
53
|
+
path: ["meta", "project"],
|
|
54
|
+
message: "meta.project 必须 kebab-case slug:首字母 a-z,只含 a-z 0-9 -,长度 2-64",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!slugPattern.test(meta.feature)) {
|
|
58
|
+
ctx.addIssue({
|
|
59
|
+
code: z.ZodIssueCode.custom,
|
|
60
|
+
path: ["meta", "feature"],
|
|
61
|
+
message: "meta.feature 必须 kebab-case slug:首字母 a-z,只含 a-z 0-9 -,长度 2-64",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=prd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prd.js","sourceRoot":"","sources":["../../src/schema/prd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PrdRc, ResolvedBackend } from "../config/schema.js";
|
|
2
|
+
export interface LoadedRc {
|
|
3
|
+
rc: PrdRc | null;
|
|
4
|
+
path: string | null;
|
|
5
|
+
/** True if file exists but had a problem (parse/schema/permissions). The
|
|
6
|
+
* command may still want to allow `prd config show` to print the (empty)
|
|
7
|
+
* rc skeleton — but for backend-using commands we treat this as fatal. */
|
|
8
|
+
hadError: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Loads rc and emits warnings to stderr for non-fatal issues (e.g. loose
|
|
12
|
+
* permissions). Returns a normalized result; callers that *require* rc
|
|
13
|
+
* should also use {@link requireBackend}.
|
|
14
|
+
*
|
|
15
|
+
* `pathOverride` exists so tests can pass an isolated rc fixture without
|
|
16
|
+
* polluting $HOME.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadRcWithWarn(pathOverride?: string): LoadedRc;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a backend or render an i18n-friendly error to stderr. Returns null
|
|
21
|
+
* if not resolvable — caller should `return 1` immediately.
|
|
22
|
+
*/
|
|
23
|
+
export declare function requireBackend(rc: PrdRc | null, opts?: {
|
|
24
|
+
backend?: string;
|
|
25
|
+
}, env?: NodeJS.ProcessEnv): ResolvedBackend | null;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared by sync commands — single place to (1) load rc with the
|
|
3
|
+
* permission-warning UX, (2) resolve backend with the env-precedence rules,
|
|
4
|
+
* and (3) render a friendly stderr message + return null when no backend.
|
|
5
|
+
*
|
|
6
|
+
* Centralizes the "no backend" / "no token" / "url-without-token" UX so the
|
|
7
|
+
* 7 sync commands aren't each re-implementing the same 30-line preamble.
|
|
8
|
+
*/
|
|
9
|
+
import { loadRc, resolveBackend } from "../config/rc.js";
|
|
10
|
+
import { getMessages, readLocale } from "../i18n/index.js";
|
|
11
|
+
import * as c from "../formatter/colors.js";
|
|
12
|
+
/**
|
|
13
|
+
* Loads rc and emits warnings to stderr for non-fatal issues (e.g. loose
|
|
14
|
+
* permissions). Returns a normalized result; callers that *require* rc
|
|
15
|
+
* should also use {@link requireBackend}.
|
|
16
|
+
*
|
|
17
|
+
* `pathOverride` exists so tests can pass an isolated rc fixture without
|
|
18
|
+
* polluting $HOME.
|
|
19
|
+
*/
|
|
20
|
+
export function loadRcWithWarn(pathOverride) {
|
|
21
|
+
const m = getMessages(readLocale());
|
|
22
|
+
const r = loadRc(pathOverride);
|
|
23
|
+
if (r.ok)
|
|
24
|
+
return { rc: r.rc, path: r.path, hadError: false };
|
|
25
|
+
if (r.reason === "missing")
|
|
26
|
+
return { rc: null, path: null, hadError: false };
|
|
27
|
+
if (r.reason === "permissions") {
|
|
28
|
+
process.stderr.write(c.warn(m.rc_chmod_warn({ mode: r.mode })) + "\n");
|
|
29
|
+
return { rc: null, path: null, hadError: true };
|
|
30
|
+
}
|
|
31
|
+
if (r.reason === "schema" || r.reason === "parse") {
|
|
32
|
+
process.stderr.write(c.fail("✗ ") + m.rc_invalid({ message: r.message }) + "\n");
|
|
33
|
+
return { rc: null, path: null, hadError: true };
|
|
34
|
+
}
|
|
35
|
+
return { rc: null, path: null, hadError: true };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a backend or render an i18n-friendly error to stderr. Returns null
|
|
39
|
+
* if not resolvable — caller should `return 1` immediately.
|
|
40
|
+
*/
|
|
41
|
+
export function requireBackend(rc, opts = {}, env = process.env) {
|
|
42
|
+
const m = getMessages(readLocale());
|
|
43
|
+
const r = resolveBackend(rc, opts, env);
|
|
44
|
+
if (r.ok)
|
|
45
|
+
return r.backend;
|
|
46
|
+
if (r.reason === "no_backend") {
|
|
47
|
+
process.stderr.write(c.fail("✗ ") + m.backend_not_configured + "\n");
|
|
48
|
+
}
|
|
49
|
+
else if (r.reason === "unknown_name") {
|
|
50
|
+
process.stderr.write(c.fail("✗ ") + m.backend_unknown({ name: r.name }) + "\n");
|
|
51
|
+
}
|
|
52
|
+
else if (r.reason === "no_token") {
|
|
53
|
+
process.stderr.write(c.fail("✗ ") +
|
|
54
|
+
m.backend_unknown({ name: r.name }) +
|
|
55
|
+
" (no token — run `prd login`)\n");
|
|
56
|
+
}
|
|
57
|
+
else if (r.reason === "env_url_without_token") {
|
|
58
|
+
process.stderr.write(c.fail("✗ ") + m.rc_env_url_without_token + "\n");
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=cliRcHelpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cliRcHelpers.js","sourceRoot":"","sources":["../../src/sync/cliRcHelpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict stash on push 409 (design §6.12).
|
|
3
|
+
*
|
|
4
|
+
* .prd/.conflicts/<slug>-<ISO_TS>/
|
|
5
|
+
* ├── local.yaml — pre-overwrite snapshot of working/prd.yaml
|
|
6
|
+
* ├── remote.yaml — backend yaml at conflict time
|
|
7
|
+
* └── README.md — user-facing merge instructions
|
|
8
|
+
*
|
|
9
|
+
* Side effect (design intent — "working/ ends in a clean state == remote"):
|
|
10
|
+
* - working/prd.yaml is overwritten with remoteYaml. After stash, working/
|
|
11
|
+
* is byte-identical to remote, so the caller's follow-up sync.json
|
|
12
|
+
* rewrite (base_version_id = remote etag, last_synced_hash = sha256(
|
|
13
|
+
* remote)) puts the workspace in a state where `prd push` will not loop
|
|
14
|
+
* on the same 409. The user's local edits are preserved in
|
|
15
|
+
* `<dir>/local.yaml` and they re-apply by hand before pushing again.
|
|
16
|
+
*
|
|
17
|
+
* Returns the absolute conflict dir path so callers can include it in the
|
|
18
|
+
* 409 error message + render the "see README" hint.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
/**
|
|
23
|
+
* Sanitize ISO timestamp into a filesystem-safe directory name. Colon is
|
|
24
|
+
* unsafe on Windows/macOS HFS, and `:`/`.` in `2026-05-14T10:30:45.123Z`
|
|
25
|
+
* confuses tab-completion in some shells. Replace with `-`.
|
|
26
|
+
*/
|
|
27
|
+
function tsForDir() {
|
|
28
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
29
|
+
}
|
|
30
|
+
export function stashConflict(cwd, slug, payload) {
|
|
31
|
+
const ts = tsForDir();
|
|
32
|
+
const dir = join(cwd, ".prd", ".conflicts", `${slug}-${ts}`);
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
writeFileSync(join(dir, "local.yaml"), payload.localYaml, "utf8");
|
|
35
|
+
writeFileSync(join(dir, "remote.yaml"), payload.remoteYaml, "utf8");
|
|
36
|
+
writeFileSync(join(dir, "README.md"), renderReadme(slug, ts, payload), "utf8");
|
|
37
|
+
// Design §6.12 step 2: overwrite working/ with remote so the workspace is
|
|
38
|
+
// in a clean state == remote. User's edits are preserved in <dir>/local.yaml.
|
|
39
|
+
// Sibling responsibility (push.ts): rewrite sync.json so base_version_id
|
|
40
|
+
// matches the new remote — otherwise the next push will still 409 on the
|
|
41
|
+
// stale base.
|
|
42
|
+
const workingPath = join(cwd, ".prd", "features", slug, "working", "prd.yaml");
|
|
43
|
+
const parent = dirname(workingPath);
|
|
44
|
+
if (!existsSync(parent))
|
|
45
|
+
mkdirSync(parent, { recursive: true });
|
|
46
|
+
writeFileSync(workingPath, payload.remoteYaml, "utf8");
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
function renderReadme(slug, ts, payload) {
|
|
50
|
+
return `# Conflict snapshot for ${slug} at ${ts}
|
|
51
|
+
|
|
52
|
+
Local push failed because remote already moved to a new version${payload.remoteVersionId ? ` (remote=${payload.remoteVersionId})` : ""}${payload.baseVersionId ? `; local base was ${payload.baseVersionId}` : ""}.
|
|
53
|
+
|
|
54
|
+
What just happened:
|
|
55
|
+
- \`local.yaml\` is a backup of your working/prd.yaml as it was before this push.
|
|
56
|
+
- \`remote.yaml\` is the backend's current yaml at conflict time.
|
|
57
|
+
- Your working/prd.yaml has been overwritten with \`remote.yaml\` so the
|
|
58
|
+
workspace is in a clean state that matches the backend. Your edits are
|
|
59
|
+
NOT lost — they live in \`local.yaml\` until you re-apply them.
|
|
60
|
+
|
|
61
|
+
To resolve:
|
|
62
|
+
1. Diff your edits vs remote: \`diff -u remote.yaml local.yaml\`
|
|
63
|
+
(or \`diff -u ../../features/${slug}/working/prd.yaml local.yaml\` —
|
|
64
|
+
working/ is now identical to remote.yaml).
|
|
65
|
+
2. Re-apply the changes you still want into
|
|
66
|
+
\`../../features/${slug}/working/prd.yaml\`.
|
|
67
|
+
3. Run \`prd validate --feature ${slug}\`, then \`prd push ${slug}\`.
|
|
68
|
+
|
|
69
|
+
This snapshot will not be deleted automatically. Remove when done.
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=conflicts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conflicts.js","sourceRoot":"","sources":["../../src/sync/conflicts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAS1C;;;;GAIG;AACH,SAAS,QAAQ;IACf,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,GAAW,EACX,IAAY,EACZ,OAAwB;IAExB,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAC;IACtB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;IAC7D,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAClE,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACpE,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,YAAY,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;IAE/E,0EAA0E;IAC1E,8EAA8E;IAC9E,yEAAyE;IACzE,yEAAyE;IACzE,cAAc;IACd,MAAM,WAAW,GAAG,IAAI,CACtB,GAAG,EACH,MAAM,EACN,UAAU,EACV,IAAI,EACJ,SAAS,EACT,UAAU,CACX,CAAC;IACF,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACpC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,aAAa,CAAC,WAAW,EAAE,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAEvD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,YAAY,CACnB,IAAY,EACZ,EAAU,EACV,OAAwB;IAExB,OAAO,2BAA2B,IAAI,OAAO,EAAE;;iEAG7C,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,YAAY,OAAO,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,EACrE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,oBAAoB,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;kCAW3C,IAAI;;;sBAGhB,IAAI;kCACQ,IAAI,uBAAuB,IAAI;;;CAGhE,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function asyncHeadProbe(slug: string | undefined, cwd: string, fetchFn?: typeof fetch): Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async HEAD probe (T6, design §6.10).
|
|
3
|
+
*
|
|
4
|
+
* Fire-and-forget. The contract is strict:
|
|
5
|
+
* - Never throws (catch-all swallow).
|
|
6
|
+
* - Never blocks: 3s timeout via Promise.race.
|
|
7
|
+
* - Never affects exit code of the parent command.
|
|
8
|
+
* - Only side effect: optional `console.warn` to stderr if remote ETag
|
|
9
|
+
* differs from local sync.json.base_version_id.
|
|
10
|
+
*
|
|
11
|
+
* The wiring lives in validate/freeze/context/diff drivers — `prd refresh`
|
|
12
|
+
* is explicitly excluded (design §15.1 O1).
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { loadRc, resolveBackend } from "../config/rc.js";
|
|
17
|
+
import { readSyncJson } from "./syncJson.js";
|
|
18
|
+
import { parseMetaScope, projectUrl } from "../api/client.js";
|
|
19
|
+
import { getMessages, readLocale } from "../i18n/index.js";
|
|
20
|
+
import * as c from "../formatter/colors.js";
|
|
21
|
+
const PROBE_TIMEOUT_MS = 3000;
|
|
22
|
+
export async function asyncHeadProbe(slug, cwd,
|
|
23
|
+
// Test seam: allow tests to inject a fake fetch and assert the warning was
|
|
24
|
+
// (or was not) emitted. Default `globalThis.fetch` keeps prod behavior.
|
|
25
|
+
fetchFn = globalThis.fetch) {
|
|
26
|
+
if (!slug)
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
const rcResult = loadRc();
|
|
30
|
+
const rc = rcResult.ok ? rcResult.rc : null;
|
|
31
|
+
const resolved = resolveBackend(rc, {});
|
|
32
|
+
if (!resolved.ok)
|
|
33
|
+
return; // no backend → no noise
|
|
34
|
+
const sync = readSyncJson(cwd, slug);
|
|
35
|
+
if (!sync)
|
|
36
|
+
return; // never pulled/pushed → no base to compare against
|
|
37
|
+
// v1.1 scope resolution: prefer working/prd.yaml meta, fall back to
|
|
38
|
+
// sync.json.team/project. Without either we cannot form the scoped URL —
|
|
39
|
+
// silently skip (probe is best-effort by design).
|
|
40
|
+
let scopeTeam;
|
|
41
|
+
let scopeProject;
|
|
42
|
+
const yamlPath = join(cwd, ".prd", "features", slug, "working", "prd.yaml");
|
|
43
|
+
if (existsSync(yamlPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const s = parseMetaScope(readFileSync(yamlPath, "utf8"));
|
|
46
|
+
scopeTeam = s.team;
|
|
47
|
+
scopeProject = s.project;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// v0.1 yaml — fall through to sync.json hint.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!scopeTeam || !scopeProject) {
|
|
54
|
+
scopeTeam = sync.team;
|
|
55
|
+
scopeProject = sync.project;
|
|
56
|
+
}
|
|
57
|
+
if (!scopeTeam || !scopeProject)
|
|
58
|
+
return;
|
|
59
|
+
const url = resolved.backend.url +
|
|
60
|
+
projectUrl(scopeTeam, scopeProject, "features", slug);
|
|
61
|
+
const ctrl = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => ctrl.abort(), PROBE_TIMEOUT_MS);
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await fetchFn(url, {
|
|
66
|
+
method: "HEAD",
|
|
67
|
+
headers: { Authorization: `Bearer ${resolved.backend.token}` },
|
|
68
|
+
signal: ctrl.signal,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
if (!res.ok)
|
|
75
|
+
return; // 404 / 401 / 5xx → silently skip
|
|
76
|
+
const remoteId = res.headers.get("ETag") ?? res.headers.get("etag");
|
|
77
|
+
if (!remoteId)
|
|
78
|
+
return;
|
|
79
|
+
// Server may quote the ETag per RFC 7232 — strip optional quotes.
|
|
80
|
+
const normalized = remoteId.replace(/^"|"$/g, "");
|
|
81
|
+
if (normalized !== sync.base_version_id) {
|
|
82
|
+
const m = getMessages(readLocale());
|
|
83
|
+
process.stderr.write(c.warn(m.head_probe_drift_warn({
|
|
84
|
+
slug,
|
|
85
|
+
remote: normalized,
|
|
86
|
+
base: sync.base_version_id,
|
|
87
|
+
})) + "\n");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Any error (DNS, abort, network, schema crash) — swallow. Local command
|
|
92
|
+
// must continue unaffected.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=headProbe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headProbe.js","sourceRoot":"","sources":["../../src/sync/headProbe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror of server `yaml/normalize.ts` (T5). CLI must hash yaml identically
|
|
3
|
+
* before comparing with sync.json.last_synced_hash, otherwise a CRLF-only
|
|
4
|
+
* delta from `git checkout` on Windows would flag working/ as "dirty" even
|
|
5
|
+
* when the bytes are semantically identical.
|
|
6
|
+
*
|
|
7
|
+
* Duplicated rather than imported from server to keep CLI runnable
|
|
8
|
+
* standalone (no server runtime dep at runtime).
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeLineEndings(s: string): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror of server `yaml/normalize.ts` (T5). CLI must hash yaml identically
|
|
3
|
+
* before comparing with sync.json.last_synced_hash, otherwise a CRLF-only
|
|
4
|
+
* delta from `git checkout` on Windows would flag working/ as "dirty" even
|
|
5
|
+
* when the bytes are semantically identical.
|
|
6
|
+
*
|
|
7
|
+
* Duplicated rather than imported from server to keep CLI runnable
|
|
8
|
+
* standalone (no server runtime dep at runtime).
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeLineEndings(s) {
|
|
11
|
+
return s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=normalize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../src/sync/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,CAAS;IAC5C,OAAO,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SyncJson {
|
|
2
|
+
base_version_id: string;
|
|
3
|
+
last_synced_hash: string;
|
|
4
|
+
last_pushed_at?: string;
|
|
5
|
+
last_pulled_at?: string;
|
|
6
|
+
backend_url: string;
|
|
7
|
+
/**
|
|
8
|
+
* v1.1 (spec §4.2): record the team/project the feature was last synced
|
|
9
|
+
* against, so fresh `prd pull` and `prd status` for a slug with no local
|
|
10
|
+
* yaml can still route the request without --team/--project flags.
|
|
11
|
+
* Optional for forward-compat with v1.0 sync.json files.
|
|
12
|
+
*/
|
|
13
|
+
team?: string;
|
|
14
|
+
project?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function syncJsonPath(cwd: string, slug: string): string;
|
|
17
|
+
export declare function readSyncJson(cwd: string, slug: string): SyncJson | null;
|
|
18
|
+
export declare function writeSyncJson(cwd: string, slug: string, sync: SyncJson): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sync.json read/write — one per feature slug, lives under
|
|
3
|
+
* `<workspace>/.prd/features/<slug>/working/sync.json`. Local state only,
|
|
4
|
+
* gitignored. Shape locked by design §6.11.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { writeFileAtomic } from "../fs/atomicWrite.js";
|
|
9
|
+
export function syncJsonPath(cwd, slug) {
|
|
10
|
+
return join(cwd, ".prd", "features", slug, "working", "sync.json");
|
|
11
|
+
}
|
|
12
|
+
export function readSyncJson(cwd, slug) {
|
|
13
|
+
const path = syncJsonPath(cwd, slug);
|
|
14
|
+
if (!existsSync(path))
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
18
|
+
// Minimal duck-typing — caller is responsible for treating malformed as
|
|
19
|
+
// "no sync state" rather than crashing. We don't want a corrupted
|
|
20
|
+
// sync.json to brick `prd push`.
|
|
21
|
+
if (typeof data !== "object" || data === null)
|
|
22
|
+
return null;
|
|
23
|
+
if (typeof data.base_version_id !== "string")
|
|
24
|
+
return null;
|
|
25
|
+
if (typeof data.last_synced_hash !== "string")
|
|
26
|
+
return null;
|
|
27
|
+
if (typeof data.backend_url !== "string")
|
|
28
|
+
return null;
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function writeSyncJson(cwd, slug, sync) {
|
|
36
|
+
const path = syncJsonPath(cwd, slug);
|
|
37
|
+
// T6-MN1 (v1.7) — atomic tempfile + rename so a crash mid-write leaves the
|
|
38
|
+
// previous sync state intact rather than producing a truncated sync.json
|
|
39
|
+
// that bricks `prd push/pull`.
|
|
40
|
+
writeFileAtomic(path, JSON.stringify(sync, null, 2) + "\n");
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=syncJson.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncJson.js","sourceRoot":"","sources":["../../src/sync/syncJson.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAkBvD,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,IAAY;IACpD,OAAO,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,IAAY;IACpD,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACrC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QACpD,wEAAwE;QACxE,kEAAkE;QAClE,iCAAiC;QACjC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAC3D,IAAI,OAAO,IAAI,CAAC,eAAe,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC1D,IAAI,OAAO,IAAI,CAAC,gBAAgB,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3D,IAAI,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACtD,OAAO,IAAgB,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,GAAW,EACX,IAAY,EACZ,IAAc;IAEd,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACrC,2EAA2E;IAC3E,yEAAyE;IACzE,+BAA+B;IAC/B,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAC9D,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type ValidationSeverity = "error" | "warning" | "info";
|
|
2
|
+
/**
|
|
3
|
+
* 单条校验问题。code 与 i18n key 一一对应;details 提供 i18n 模板变量。
|
|
4
|
+
*/
|
|
5
|
+
export interface ValidationIssue {
|
|
6
|
+
severity: ValidationSeverity;
|
|
7
|
+
code: string;
|
|
8
|
+
feature: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
details: Record<string, string | number>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 单个 feature 的校验结果。
|
|
14
|
+
*/
|
|
15
|
+
export interface FeatureResult {
|
|
16
|
+
feature: string;
|
|
17
|
+
filePath: string;
|
|
18
|
+
status: "pass" | "fail" | "skip";
|
|
19
|
+
/** 已完整的必填业务模块数(out of 15)。skip 时为 0。 */
|
|
20
|
+
modulesComplete: number;
|
|
21
|
+
/** 业务模块总数,固定 15(本期 schema v0.2,见 SCHEMA_VERSION_CURRENT)。 */
|
|
22
|
+
modulesTotal: number;
|
|
23
|
+
/** schema_version 实际值(便于报告里展示)。skip / parse_error 时为 ""。 */
|
|
24
|
+
schemaVersion: string;
|
|
25
|
+
issues: ValidationIssue[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 所有 feature 的汇总结果。
|
|
29
|
+
*/
|
|
30
|
+
export interface ValidationResult {
|
|
31
|
+
results: FeatureResult[];
|
|
32
|
+
summary: {
|
|
33
|
+
passed: number;
|
|
34
|
+
failed: number;
|
|
35
|
+
skipped: number;
|
|
36
|
+
};
|
|
37
|
+
exitCode: 0 | 1 | 2;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* discover 阶段的输入分发结果。
|
|
41
|
+
*/
|
|
42
|
+
export type DiscoverInput = {
|
|
43
|
+
kind: "scan_all";
|
|
44
|
+
} | {
|
|
45
|
+
kind: "single_feature";
|
|
46
|
+
slug: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "single_path";
|
|
49
|
+
path: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* discover 阶段的输出 —— 决定要校验哪些目标。
|
|
53
|
+
*/
|
|
54
|
+
export interface DiscoverOutput {
|
|
55
|
+
targets: Array<{
|
|
56
|
+
feature: string;
|
|
57
|
+
filePath: string;
|
|
58
|
+
exists: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
/** 致命错误(如 .prd 目录缺失、用户路径不存在)。非空时 main 流程直接退 2。 */
|
|
61
|
+
fatal?: {
|
|
62
|
+
code: string;
|
|
63
|
+
details: Record<string, string>;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 业务模块的固定列表(顺序 = 输出顺序)。15 个。
|
|
68
|
+
*/
|
|
69
|
+
export declare const BUSINESS_MODULES: readonly ["meta", "background_and_goals", "users_and_roles", "in_scope", "out_of_scope", "entities", "main_flow", "state_permissions", "business_rules", "scenarios", "edge_cases", "ui_interaction", "data_and_api", "acceptance_criteria", "open_questions"];
|
|
70
|
+
export type BusinessModule = (typeof BUSINESS_MODULES)[number];
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 业务模块的固定列表(顺序 = 输出顺序)。15 个。
|
|
3
|
+
*/
|
|
4
|
+
export const BUSINESS_MODULES = [
|
|
5
|
+
"meta",
|
|
6
|
+
"background_and_goals",
|
|
7
|
+
"users_and_roles",
|
|
8
|
+
"in_scope",
|
|
9
|
+
"out_of_scope",
|
|
10
|
+
"entities",
|
|
11
|
+
"main_flow",
|
|
12
|
+
"state_permissions",
|
|
13
|
+
"business_rules",
|
|
14
|
+
"scenarios",
|
|
15
|
+
"edge_cases",
|
|
16
|
+
"ui_interaction",
|
|
17
|
+
"data_and_api",
|
|
18
|
+
"acceptance_criteria",
|
|
19
|
+
"open_questions",
|
|
20
|
+
];
|
|
21
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA+DA;;GAEG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,MAAM;IACN,sBAAsB;IACtB,iBAAiB;IACjB,UAAU;IACV,cAAc;IACd,UAAU;IACV,WAAW;IACX,mBAAmB;IACnB,gBAAgB;IAChB,WAAW;IACX,YAAY;IACZ,gBAAgB;IAChB,cAAc;IACd,qBAAqB;IACrB,gBAAgB;CACR,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function validateBlocking(obj, feature) {
|
|
2
|
+
if (!obj || typeof obj !== "object")
|
|
3
|
+
return [];
|
|
4
|
+
const data = obj;
|
|
5
|
+
const arr = data.open_questions;
|
|
6
|
+
if (!Array.isArray(arr))
|
|
7
|
+
return [];
|
|
8
|
+
const issues = [];
|
|
9
|
+
arr.forEach((q, i) => {
|
|
10
|
+
if (q?.severity === "blocking") {
|
|
11
|
+
issues.push({
|
|
12
|
+
severity: "error",
|
|
13
|
+
code: "blocking_question",
|
|
14
|
+
feature,
|
|
15
|
+
path: `open_questions[${i}]`,
|
|
16
|
+
details: {
|
|
17
|
+
id: typeof q.id === "string" ? q.id : "",
|
|
18
|
+
question: typeof q.question === "string" ? q.question : "",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return issues;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=blocking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocking.js","sourceRoot":"","sources":["../../src/validators/blocking.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,gBAAgB,CAC9B,GAAY,EACZ,OAAe;IAEf,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IAC/C,MAAM,IAAI,GAAG,GAA8B,CAAC;IAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC;IAChC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAEnC,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAM,EAAE,CAAS,EAAE,EAAE;QAChC,IAAI,CAAC,EAAE,QAAQ,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC;gBACV,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,mBAAmB;gBACzB,OAAO;gBACP,IAAI,EAAE,kBAAkB,CAAC,GAAG;gBAC5B,OAAO,EAAE;oBACP,EAAE,EAAE,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;oBACxC,QAAQ,EAAE,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;iBAC3D;aACF,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const ID_BUCKETS = [
|
|
2
|
+
["entities", "entities"],
|
|
3
|
+
["in_scope", "in_scope"],
|
|
4
|
+
["business_rules", "business_rules"],
|
|
5
|
+
["scenarios", "scenarios"],
|
|
6
|
+
["edge_cases", "edge_cases"],
|
|
7
|
+
["ui_interaction", "ui_interaction"],
|
|
8
|
+
["acceptance_criteria", "acceptance_criteria"],
|
|
9
|
+
["open_questions", "open_questions"],
|
|
10
|
+
];
|
|
11
|
+
export function validateDuplicateIds(obj, feature) {
|
|
12
|
+
if (!obj || typeof obj !== "object")
|
|
13
|
+
return [];
|
|
14
|
+
const data = obj;
|
|
15
|
+
const seen = new Map(); // id → first_seen_path
|
|
16
|
+
const issues = [];
|
|
17
|
+
function check(item, path) {
|
|
18
|
+
if (!item || typeof item.id !== "string")
|
|
19
|
+
return;
|
|
20
|
+
const id = item.id;
|
|
21
|
+
if (seen.has(id)) {
|
|
22
|
+
issues.push({
|
|
23
|
+
severity: "error",
|
|
24
|
+
code: "duplicate_id",
|
|
25
|
+
feature,
|
|
26
|
+
path,
|
|
27
|
+
details: { id, first_seen_path: seen.get(id) },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
seen.set(id, path);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const [key, label] of ID_BUCKETS) {
|
|
35
|
+
const arr = data[key];
|
|
36
|
+
if (!Array.isArray(arr))
|
|
37
|
+
continue;
|
|
38
|
+
arr.forEach((item, i) => check(item, `${label}[${i}].id`));
|
|
39
|
+
}
|
|
40
|
+
// data_and_api 子数组
|
|
41
|
+
const dao = data.data_and_api;
|
|
42
|
+
if (dao) {
|
|
43
|
+
const subs = [
|
|
44
|
+
["data_constraints", "data_and_api.data_constraints"],
|
|
45
|
+
["api_constraints", "data_and_api.api_constraints"],
|
|
46
|
+
["performance_constraints", "data_and_api.performance_constraints"],
|
|
47
|
+
["security_constraints", "data_and_api.security_constraints"],
|
|
48
|
+
];
|
|
49
|
+
for (const [k, label] of subs) {
|
|
50
|
+
const arr = dao[k];
|
|
51
|
+
if (!Array.isArray(arr))
|
|
52
|
+
continue;
|
|
53
|
+
arr.forEach((item, i) => check(item, `${label}[${i}].id`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return issues;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=duplicate-ids.js.map
|