@produtype/core 0.98.0 → 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 CHANGED
@@ -21,20 +21,33 @@ ProdKit inspects a target project and detects:
21
21
  - GDPR/privacy signals
22
22
  - Security hardening signals
23
23
  - Upload exposure signals
24
- - Billing/Stripe signals
24
+ - Billing signals: payment processors declared in any manifest this reads
25
25
  - Observability/logging signals
26
26
  - Background jobs/queue signals
27
27
  - Deployment readiness signals
28
28
 
29
29
  Then it builds a structured production-readiness report with:
30
30
 
31
- - Overall score (0-100)
32
- - Maturity level (`prototype`, `early`, `partial`, `production_ready`)
31
+ - Overall score (0-100), or `null` where too little of the repository could be read
32
+ to characterise it
33
+ - Maturity level (`inconclusive`, `prototype`, `early`, `partial`, `production_ready`)
33
34
  - Findings grouped by category
34
35
  - Critical issues and warnings
35
36
  - Passed checks
36
37
  - Suggested next steps
37
- - Technical evidence for each finding
38
+ - Technical evidence for each finding — a file and a line you can open, not a summary
39
+
40
+ Two things the report says about its own reading, because a finding is only worth what
41
+ the reading behind it is:
42
+
43
+ - **How deeply each language was read.** A syntax tree answered the question, or a
44
+ keyword search did, or the language was not read at all — and the report names which,
45
+ per language. The optional `typescript` peer dependency decides whether JavaScript and
46
+ TypeScript are parsed; without it they are searched as text, and the report says so
47
+ rather than claiming otherwise.
48
+ - **Which questions could not be asked.** A check whose answer depends on a reader that
49
+ could not run comes back `unknown`, never `passed` and never `missing`. Blindness can
50
+ turn a verdict into no verdict; it is not allowed to turn one into its opposite.
38
51
 
39
52
  From the report, ProdKit can also build a deterministic remediation plan with phases, task priorities, effort estimates, and test suggestions.
40
53
 
@@ -145,7 +158,11 @@ Profile warning:
145
158
 
146
159
  Inconclusive assessments:
147
160
 
148
- - If no stack signals and no package manifests are detected, the report is marked **inconclusive** and the score is capped at 39 (`prototype`). An unrecognized project is never scored as production ready.
161
+ - Where nothing identifies the repository, where too few checks reach a verdict, or
162
+ where most of it is written in a language this cannot read, the report is marked
163
+ **inconclusive** and there is no score at all: `overallScore` is `null` and the
164
+ maturity level is `inconclusive`. It used to cap the score at 39 instead, which was
165
+ a number standing where an answer was missing.
149
166
 
150
167
  ## Examples
151
168
 
@@ -220,8 +237,12 @@ its own.
220
237
  ## Current limitations
221
238
 
222
239
  - Deterministic heuristics only: the AI layer is a separate package (see above)
223
- - Signal-based stack coverage across common Node/Python/JS frameworks + fallback
224
- - Signal-based detection can produce false positives/negatives
240
+ - JavaScript and TypeScript are parsed when the optional `typescript` peer dependency is
241
+ installed; every other language is read as text, matched against what its frameworks
242
+ and its standard library define. The report states which of the two it did.
243
+ - Signal-based detection can produce false positives and false negatives. Where a
244
+ reading rests on something this could not check, the answer is `unknown` rather than a
245
+ guess.
225
246
  - Plan output is deterministic and read-only only
226
247
  - No cloud dashboard or UI: this package is the CLI and the library
227
248
 
@@ -5,6 +5,7 @@ const textSearch_1 = require("../utils/textSearch");
5
5
  const lookupTables_1 = require("./structural/lookupTables");
6
6
  const secretArguments_1 = require("./structural/secretArguments");
7
7
  const readingDepth_1 = require("./readingDepth");
8
+ const djangoSettings_1 = require("./djangoSettings");
8
9
  const WEAK_SECRET_VALUE_RE = /(changeme|your[_-]?secret|fallback-secret(?:-change-in-production)?|change[_-]in[_-]production|your_jwt_secret_key_change_in_production|local[-_]?secret|development[-_]?secret|dev[-_]?secret|not[_-]?for[_-]?production|test123|secret)/i;
9
10
  const FALLBACK_SECRET_RE = /(jwt_secret|secret_key|session_secret)\s*[:=]\s*['"][^'"]*(changeme|your[_-]?secret|fallback-secret(?:-change-in-production)?|change[_-]in[_-]production|your_jwt_secret_key_change_in_production|local[-_]?secret|development[-_]?secret|dev[-_]?secret|not[_-]?for[_-]?production|test123|secret)[^'"]*['"]/i;
10
11
  const ENV_FALLBACK_RE = /(process\.env\.(JWT_SECRET|SECRET_KEY|SESSION_SECRET)\s*(\|\||\?\?)\s*['"][^'"]*(changeme|your[_-]?secret|fallback-secret(?:-change-in-production)?|change[_-]in[_-]production|your_jwt_secret_key_change_in_production|local[-_]?secret|development[-_]?secret|dev[-_]?secret|not[_-]?for[_-]?production|test123|secret)[^'"]*['"])/i;
@@ -148,12 +149,25 @@ async function detectEnv(ctx) {
148
149
  * argue with — silence is not.
149
150
  */
150
151
  const secretSinksUnasked = (0, readingDepth_1.wentUnasked)(secretArguments, sourceFiles) && (await (0, secretArguments_1.anyFileReachesASecretSink)(ctx.root, sourceFiles));
152
+ /**
153
+ * A settings module the application does not run is not where its secret lives.
154
+ *
155
+ * pretix keeps `SECRET_KEY = "build-time-secret-key"` in `_build_settings.py`, a
156
+ * module named only by its packaging script, while `manage.py` and `wsgi.py` both
157
+ * name `pretix.settings`. That literal was the one `critical` in its report.
158
+ *
159
+ * Only applied when the entry points say something: with no `manage.py` to read,
160
+ * nothing is excused and the behaviour is what it was.
161
+ */
162
+ const runningSettings = await (0, djangoSettings_1.settingsModulesTheAppRuns)(ctx);
163
+ const isAnotherSettingsModule = (file) => runningSettings.length > 0 && (0, djangoSettings_1.looksLikeDjangoSettings)(file) && !runningSettings.includes(file);
151
164
  const weakHits = fallbackHits.filter((m) => WEAK_SECRET_VALUE_RE.test(m.snippet)
152
165
  && SECRET_ASSIGNMENT_CONTEXT_RE.test(m.snippet)
153
166
  && !namesItself(m.snippet)
154
167
  && !valueIsAnIdentifier(m.snippet)
155
168
  && !REDACTED_VALUE_RE.test(m.snippet)
156
- && !isTableEntry(m));
169
+ && !isTableEntry(m)
170
+ && !isAnotherSettingsModule(m.file));
157
171
  for (const m of weakHits) {
158
172
  const hitEvidence = { type: 'snippet', value: m.snippet, file: m.file, line: m.line };
159
173
  evidence.push(hitEvidence);
@@ -18,3 +18,23 @@ export declare function findDjangoSettings(ctx: DetectContext): Promise<{
18
18
  file: string;
19
19
  text: string;
20
20
  } | null>;
21
+ /**
22
+ * The settings module the application actually runs, named by Django's own entry points.
23
+ *
24
+ * `manage.py`, `wsgi.py` and `asgi.py` each set `DJANGO_SETTINGS_MODULE`, and that is
25
+ * the framework saying which configuration is the project's. Everything else called
26
+ * `settings` is somebody's variant: a test harness, a build-time module, a sample.
27
+ *
28
+ * pretix is the measured case. `src/pretix/_build_settings.py` holds
29
+ * `SECRET_KEY = "build-time-secret-key"` and is named only by `src/pretix/_build.py`,
30
+ * a packaging script; `manage.py` and `wsgi.py` both name `pretix.settings`. That
31
+ * literal was the single `critical` in pretix's report — the severity that bars a
32
+ * report from the top band — raised against a line that never serves a request.
33
+ *
34
+ * A name rule could not do this: `configuration_testing.py` was caught by one in
35
+ * 0.80.0, and `_build_settings.py` would need "build" to mean something, which it does
36
+ * not. The entry point is a fact, not a word.
37
+ */
38
+ export declare function settingsModulesTheAppRuns(ctx: DetectContext): Promise<string[]>;
39
+ /** Whether a path is the sort of file a Django project keeps its configuration in. */
40
+ export declare function looksLikeDjangoSettings(file: string): boolean;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.findDjangoSettings = findDjangoSettings;
4
+ exports.settingsModulesTheAppRuns = settingsModulesTheAppRuns;
5
+ exports.looksLikeDjangoSettings = looksLikeDjangoSettings;
4
6
  const readTextFileSafe_1 = require("../utils/readTextFileSafe");
5
7
  /**
6
8
  * A settings file that is Django's, rather than one that shares its name.
@@ -31,3 +33,71 @@ async function findDjangoSettings(ctx) {
31
33
  }
32
34
  return null;
33
35
  }
36
+ /**
37
+ * The settings module the application actually runs, named by Django's own entry points.
38
+ *
39
+ * `manage.py`, `wsgi.py` and `asgi.py` each set `DJANGO_SETTINGS_MODULE`, and that is
40
+ * the framework saying which configuration is the project's. Everything else called
41
+ * `settings` is somebody's variant: a test harness, a build-time module, a sample.
42
+ *
43
+ * pretix is the measured case. `src/pretix/_build_settings.py` holds
44
+ * `SECRET_KEY = "build-time-secret-key"` and is named only by `src/pretix/_build.py`,
45
+ * a packaging script; `manage.py` and `wsgi.py` both name `pretix.settings`. That
46
+ * literal was the single `critical` in pretix's report — the severity that bars a
47
+ * report from the top band — raised against a line that never serves a request.
48
+ *
49
+ * A name rule could not do this: `configuration_testing.py` was caught by one in
50
+ * 0.80.0, and `_build_settings.py` would need "build" to mean something, which it does
51
+ * not. The entry point is a fact, not a word.
52
+ */
53
+ async function settingsModulesTheAppRuns(ctx) {
54
+ const entryPoints = ctx.files.all.filter((file) => /(^|\/)(manage|wsgi|asgi)\.py$/.test(file));
55
+ const modules = new Set();
56
+ for (const file of entryPoints.slice(0, 6)) {
57
+ const text = await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file);
58
+ if (!text)
59
+ continue;
60
+ for (const match of text.matchAll(/DJANGO_SETTINGS_MODULE["'\s,]+["']([\w.]+)["']/g)) {
61
+ modules.add(match[1]);
62
+ }
63
+ }
64
+ if (modules.size === 0)
65
+ return [];
66
+ const paths = [];
67
+ for (const module of modules) {
68
+ const asPath = module.replace(/\./g, '/');
69
+ for (const file of ctx.files.all) {
70
+ if (file.endsWith(`${asPath}.py`) || file.endsWith(`${asPath}/__init__.py`))
71
+ paths.push(file);
72
+ }
73
+ }
74
+ /**
75
+ * A settings package is loaded whole, through its own imports.
76
+ *
77
+ * `config.settings.production` starts with `from .base import *`, so `base.py` is as
78
+ * much the running configuration as the module named. The first version of this rule
79
+ * excused `base.py` and a fixture written for exactly that shape — a `SECRET_KEY =
80
+ * "changeme"` in the base module — went from `missing` to `passed` in the corpus
81
+ * diff, which is how the mistake surfaced within a minute of making it.
82
+ *
83
+ * Every module beside the named one, inside a directory called `settings`, counts.
84
+ * That is the shape of a split configuration, and it does not reach a sibling of a
85
+ * plain `settings.py` — which is where pretix keeps the build-time module this rule
86
+ * exists to exclude.
87
+ */
88
+ const withSiblings = new Set(paths);
89
+ for (const file of paths) {
90
+ const directory = file.slice(0, file.lastIndexOf('/'));
91
+ if (!/(^|\/)settings$/.test(directory))
92
+ continue;
93
+ for (const candidate of ctx.files.all) {
94
+ if (candidate.startsWith(`${directory}/`) && candidate.endsWith('.py'))
95
+ withSiblings.add(candidate);
96
+ }
97
+ }
98
+ return [...withSiblings];
99
+ }
100
+ /** Whether a path is the sort of file a Django project keeps its configuration in. */
101
+ function looksLikeDjangoSettings(file) {
102
+ return /(^|\/)[\w-]*settings[\w-]*\.py$/i.test(file) || /(^|\/)settings\/[\w-]+\.py$/i.test(file);
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.98.0",
3
+ "version": "1.0.0",
4
4
  "description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
5
5
  "license": "MIT",
6
6
  "bin": {